diff --git a/README.md b/README.md index 1157cfec..28cee625 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ A highly configurable drag & drop form building module for creating dynamic, res - [Installation](#installation) - [Usage](#usage) - [Framework Integration](#framework-integration) +- [Theming](#theming) - [Documentation](#documentation) - [Development](#development) - [Contributing](#contributing) @@ -161,6 +162,37 @@ Formeo can be integrated with popular frontend frameworks: - **[React Integration Guide](https://github.com/Draggable/formeo/blob/main/docs/react-integration.md)** - Custom hooks, functional components, and Context API patterns - **[Angular Integration Guide](https://github.com/Draggable/formeo/blob/main/docs/angular-integration.md)** - Services, components, and standalone patterns +## Theming + +Every color formeo uses is a `--formeo-*` CSS custom property, declared on `:root` with zero specificity. Override them on `:root` or ``: formeo appends its dialogs to `document.body`, so a rule scoped to the editor's container misses them. A narrower selector works too if it also covers `.formeo-dialog` and, when you move the controls panel with `controls.container`, `.formeo-controls`. For example, to map your own dark theme: + +```css +:root { + color-scheme: dark; + color: #fafafa; + --formeo-bg: #0a0a0a; + --formeo-bg-hover: #1a1a1a; + --formeo-text: #fafafa; + --formeo-border: #262626; + --formeo-icon: #e5e5e5; +} +``` + +- **Dark preset:** add the `formeo-dark` class to `` (or `:root`). It sets `color-scheme: dark` on that subtree, so your own native controls inside it darken too. Dialogs take the preset's background and text colors as well. +- **Your own dark theme:** set `color-scheme: dark` as well, and make sure your mapping's scope includes `.formeo-dialog`. Outside the preset, formeo leaves the dialog's own background and text color, the text color of inputs and selects, and the checkboxes to the browser, which only switches them to dark-mode colors under a dark `color-scheme`. +- **Text color:** outside the dark preset, the editor and controls containers inherit the page's `color`. Setting `--formeo-text` alone only recolors text formeo colors explicitly, not inherited labels, so set `color` on your container too. +- **Derived properties:** some defaults are computed from another color at build time and don't follow when you override that color. If you change the base, override these too: `--formeo-bg-hover`, `--formeo-overlay`, `--formeo-danger-subtle`, `--formeo-column-outline-soft`, and the `--formeo-*-highlight` / `--formeo-*-highlight-text` properties. + +The full list, with defaults, is in [`_properties.scss`](https://github.com/Draggable/formeo/blob/main/src/lib/sass/base/_properties.scss). It isn't included in the npm package. The groups, without the `--formeo-` prefix, are: + +- **Surfaces:** `bg`, `bg-hover`, `surface-muted`, `stage-bg`, `stage-shadow`, `overlay` +- **Text and icons:** `text`, `text-strong`, `text-secondary`, `text-muted`, `text-subtle`, `on-accent`, `icon` +- **Borders and focus:** `border`, `border-strong`, `focus` +- **Accents:** `primary`, `success`, `warning` and `danger` (each with a `-dark` variant), `danger-subtle`, `info`, `remove-bg` +- **Component outlines:** `{stage,row,column,field,option}-outline`, `-outline-text`, `-highlight` and `-highlight-text`, plus `column-outline-soft` + +**Visual changes from 5.1.3:** icons that hard-coded `#444` (header, paragraph and the triangles) now use `--formeo-icon`, which defaults to `#000`. The column resize-handle triangles now use the column outline color (`--formeo-column-outline-soft`, or the darker `--formeo-column-outline` on hover) instead of `#444`. + ## Documentation Comprehensive documentation is available in the [docs](https://github.com/Draggable/formeo/blob/main/docs/README.md) directory: diff --git a/biome.json b/biome.json index 6dddbffa..bad32509 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,8 @@ "!**/dist", "!**/test-results", "!**/playwright-report", - "!**/src/demo/index.html" + "!**/src/demo/index.html", + "!**/src/lib/sass/__fixtures__" ] }, "assist": { "actions": { "source": { "organizeImports": "on" } } }, diff --git a/package-lock.json b/package-lock.json index e5c27d40..b04217d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3219,9 +3219,9 @@ "license": "MIT" }, "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", "dev": true, "license": "MIT", "engines": { @@ -5017,9 +5017,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -6316,9 +6316,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -7338,9 +7338,9 @@ } }, "node_modules/npm": { - "version": "11.19.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.19.0.tgz", - "integrity": "sha512-SDd/hHg3KqHE5Ht2NHWxNYNtqCQ2pXAPLl6OtQhPyED5PHsRfrOtO199MZTIG2cQoQ1ZRI9t28shrD+2cr3AAw==", + "version": "11.20.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.20.0.tgz", + "integrity": "sha512-dF3EDFwbYN+N5RUip+ZYDe0NeURK5BgqKOcvT1iNtUYhTMTl0FwWhBuXrS7KtXyduqyTMS5aaQaregnHDAxNgw==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -7419,8 +7419,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.9.1", - "@npmcli/config": "^10.12.0", + "@npmcli/arborist": "^9.9.2", + "@npmcli/config": "^10.13.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -7444,12 +7444,12 @@ "is-cidr": "^6.0.4", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.12", - "libnpmexec": "^10.3.2", - "libnpmfund": "^7.0.26", + "libnpmdiff": "^8.1.13", + "libnpmexec": "^10.3.3", + "libnpmfund": "^7.0.27", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.12", - "libnpmpublish": "^11.2.0", + "libnpmpack": "^9.1.14", + "libnpmpublish": "^11.2.1", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", "libnpmversion": "^8.0.4", @@ -7477,7 +7477,7 @@ "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", - "tar": "^7.5.19", + "tar": "^7.5.22", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", @@ -7744,7 +7744,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.9.1", + "version": "9.9.2", "dev": true, "inBundle": true, "license": "ISC", @@ -7792,7 +7792,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.12.0", + "version": "10.13.0", "dev": true, "inBundle": true, "license": "ISC", @@ -8137,7 +8137,7 @@ } }, "node_modules/npm/node_modules/brace-expansion": { - "version": "5.0.7", + "version": "5.0.9", "dev": true, "inBundle": true, "license": "MIT", @@ -8145,7 +8145,7 @@ "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/npm/node_modules/cacache": { @@ -8429,7 +8429,7 @@ } }, "node_modules/npm/node_modules/ip-address": { - "version": "10.2.0", + "version": "10.5.0", "dev": true, "inBundle": true, "license": "MIT", @@ -8511,12 +8511,12 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.1.12", + "version": "8.1.13", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.9.1", + "@npmcli/arborist": "^9.9.2", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -8530,13 +8530,13 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.3.2", + "version": "10.3.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", - "@npmcli/arborist": "^9.9.1", + "@npmcli/arborist": "^9.9.2", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", @@ -8553,12 +8553,12 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.26", + "version": "7.0.27", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.9.1" + "@npmcli/arborist": "^9.9.2" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -8578,12 +8578,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.1.12", + "version": "9.1.14", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.9.1", + "@npmcli/arborist": "^9.9.2", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -8593,7 +8593,7 @@ } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "11.2.0", + "version": "11.2.1", "dev": true, "inBundle": true, "license": "ISC", @@ -9288,7 +9288,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.19", + "version": "7.5.22", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -9384,7 +9384,7 @@ } }, "node_modules/npm/node_modules/undici": { - "version": "6.27.0", + "version": "6.28.0", "dev": true, "inBundle": true, "license": "MIT", @@ -11766,9 +11766,9 @@ } }, "node_modules/svgo": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.3.tgz", - "integrity": "sha512-5EZD0pafXX6PphdwOGCiVLDSaV1xyuQao2blHajHLsPxr07q4mmEjdtXEWgG07ae2mIz8Ex2CDXNCTiXhy3Khw==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.4.tgz", + "integrity": "sha512-2GJ4h3rl13qYTdwllaK6QlL8tG+UrM8626V2Ylcd/yBUv2Y/EwLsYisVV6UDCRmlk+C74G8nMpxJCk0RWPdDCw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b7c73e64..2878a761 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "postbuild:demo": "node --no-warnings tools/copy-assets.mjs", "build:demo:watch": "vite build --mode demo --watch", "build:icons": "node ./tools/generate-sprite", - "lint": "biome check ./src", + "lint": "biome check ./src && node tools/check-color-literals.mjs", "lint:fix": "biome check --write ./src", "format": "biome format --write .", "test": "node --loader=./tools/svg-loader.mjs --import=./tools/__mocks__/sprite-init.mjs --experimental-test-snapshots --require ./tools/test-setup.cjs --test --no-warnings src/**/*.test.{js,mjs}", diff --git a/src/demo/js/actionButtons.js b/src/demo/js/actionButtons.js index 9054bd22..e8294f40 100644 --- a/src/demo/js/actionButtons.js +++ b/src/demo/js/actionButtons.js @@ -48,6 +48,7 @@ const editorActions = (editor, renderer) => { editData: () => { jsonEditor.setValue(JSON.stringify(editor.formData, null, 2), 1) }, + toggleDark: () => document.body.classList.toggle('formeo-dark'), } } diff --git a/src/demo/sass/demo.scss b/src/demo/sass/demo.scss index d896e980..db6059a3 100644 --- a/src/demo/sass/demo.scss +++ b/src/demo/sass/demo.scss @@ -473,3 +473,35 @@ body { } } } + +// "Toggle Dark" puts .formeo-dark on , so the demo page itself goes dark too. +body.formeo-dark { + background: linear-gradient(135deg, #18181b 0%, #1e293b 100%); + background-attachment: fixed; + color: var(--formeo-text); + + #demo-header, + #demo-header nav, + #demo-footer nav { + background-color: var(--formeo-bg); + } + + #demo-header { + border-bottom: 1px solid var(--formeo-border); + } + + #demo-footer nav { + border-top: 1px solid var(--formeo-border); + } + + #formeo-logo, + #demo-footer img { + filter: invert(1); + } + + #formData-popover[popover] { + background-color: var(--formeo-bg); + border-color: var(--formeo-border); + color: var(--formeo-text); + } +} diff --git a/src/lib/icons/formeo-sprite.svg b/src/lib/icons/formeo-sprite.svg index 2f73f02d..1544c41c 100644 --- a/src/lib/icons/formeo-sprite.svg +++ b/src/lib/icons/formeo-sprite.svg @@ -1 +1 @@ -image/svg+xmlimage/svg+xmlimage/svg+xmlimage/svg+xmlimage/svg+xmlimage/svg+xml \ No newline at end of file +image/svg+xmlimage/svg+xmlimage/svg+xmlimage/svg+xmlimage/svg+xmlimage/svg+xml \ No newline at end of file diff --git a/src/lib/icons/icon-email.svg b/src/lib/icons/icon-email.svg index aa3c9e01..a3954865 100644 --- a/src/lib/icons/icon-email.svg +++ b/src/lib/icons/icon-email.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/lib/icons/icon-hash.svg b/src/lib/icons/icon-hash.svg index d475e2d4..2d6956bc 100644 --- a/src/lib/icons/icon-hash.svg +++ b/src/lib/icons/icon-hash.svg @@ -2,5 +2,5 @@ - + diff --git a/src/lib/icons/icon-header.svg b/src/lib/icons/icon-header.svg index 7bbe2293..d75a469c 100644 --- a/src/lib/icons/icon-header.svg +++ b/src/lib/icons/icon-header.svg @@ -2,5 +2,5 @@ - + diff --git a/src/lib/icons/icon-minus.svg b/src/lib/icons/icon-minus.svg index bc96d610..aa646f0a 100644 --- a/src/lib/icons/icon-minus.svg +++ b/src/lib/icons/icon-minus.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/lib/icons/icon-paragraph.svg b/src/lib/icons/icon-paragraph.svg index a69c4a79..3cd8ee71 100644 --- a/src/lib/icons/icon-paragraph.svg +++ b/src/lib/icons/icon-paragraph.svg @@ -2,5 +2,5 @@ - + diff --git a/src/lib/icons/icon-plus.svg b/src/lib/icons/icon-plus.svg index 637f5d9f..35cf3cdd 100644 --- a/src/lib/icons/icon-plus.svg +++ b/src/lib/icons/icon-plus.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/lib/icons/icon-triangle-down.svg b/src/lib/icons/icon-triangle-down.svg index 2bc7f59f..3180ae09 100644 --- a/src/lib/icons/icon-triangle-down.svg +++ b/src/lib/icons/icon-triangle-down.svg @@ -2,5 +2,5 @@ - + diff --git a/src/lib/icons/icon-triangle-left.svg b/src/lib/icons/icon-triangle-left.svg index acda4c4c..a469df7c 100644 --- a/src/lib/icons/icon-triangle-left.svg +++ b/src/lib/icons/icon-triangle-left.svg @@ -2,5 +2,5 @@ - + diff --git a/src/lib/icons/icon-triangle-right.svg b/src/lib/icons/icon-triangle-right.svg index 41eaf6ee..e63b2ec5 100644 --- a/src/lib/icons/icon-triangle-right.svg +++ b/src/lib/icons/icon-triangle-right.svg @@ -2,5 +2,5 @@ - + diff --git a/src/lib/icons/icon-triangle-up.svg b/src/lib/icons/icon-triangle-up.svg index c8c67da5..57a0825f 100644 --- a/src/lib/icons/icon-triangle-up.svg +++ b/src/lib/icons/icon-triangle-up.svg @@ -2,5 +2,5 @@ - + diff --git a/src/lib/icons/icons.test.mjs b/src/lib/icons/icons.test.mjs new file mode 100644 index 00000000..919da8fb --- /dev/null +++ b/src/lib/icons/icons.test.mjs @@ -0,0 +1,19 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { suite, test } from 'node:test' + +const dir = new URL('./', import.meta.url) +const svgFiles = readdirSync(dir).filter(f => f.endsWith('.svg')) + +suite('icons', () => { + test('no icon hard-codes a fill or stroke color attribute', t => { + const offenders = svgFiles.filter(f => /(fill|stroke)="#[0-9a-fA-F]+"/.test(readFileSync(new URL(f, dir), 'utf8'))) + t.assert.deepStrictEqual(offenders, []) + }) + + test('the sprite has no fill or stroke color in a style attribute', t => { + const sprite = readFileSync(new URL('formeo-sprite.svg', dir), 'utf8') + const styles = sprite.match(/style="[^"]*"/g) ?? [] + const offenders = styles.filter(style => /(fill|stroke)\s*:\s*#/.test(style)) + t.assert.deepStrictEqual(offenders, []) + }) +}) diff --git a/src/lib/sass/__fixtures__/formeo-baseline.css b/src/lib/sass/__fixtures__/formeo-baseline.css new file mode 100644 index 00000000..678fc8ed --- /dev/null +++ b/src/lib/sass/__fixtures__/formeo-baseline.css @@ -0,0 +1,2419 @@ +/* + Mixins +*/ +.svg-icon { + display: inline-block; + width: 24px; + height: 24px; + pointer-events: none; +} + +.f-i-remove:hover { + fill: rgb(217, 83, 79); +} + +button[class*=-move]:hover { + background-color: rgb(153, 84, 187) !important; +} +button[class*=-move]:hover .svg-icon { + fill: #fff; +} + +button[class*=-remove]:hover { + background-color: rgb(217, 83, 79) !important; +} +button[class*=-remove]:hover .svg-icon { + fill: #fff; +} + +button[class*=-clone]:hover { + background-color: rgb(147, 197, 75) !important; +} +button[class*=-clone]:hover .svg-icon { + fill: #fff; +} + +.edit-toggle:hover { + background-color: rgb(50, 93, 136) !important; +} +.edit-toggle:hover .svg-icon { + fill: #fff; +} + +.f-autocomplete { + position: relative; +} + +.f-autocomplete-display-field { + width: 100%; +} + +.f-autocomplete-list { + background-color: #fff; + display: none; + list-style: none; + padding: 0; + position: absolute; + z-index: 2; + width: 100%; + margin: 0 -1px; + border: 1px solid #999999; + border-width: 1px 1px 0; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.5); +} +.f-autocomplete-list li { + position: relative; + border: 1px solid #999999; + display: none; + cursor: default; + padding: 4px; + margin: -1px 0 0; + border-width: 1px 0; + transition: background-color 133ms ease-in-out; + will-change: background-color; + font-size: 0.85em; +} +.f-autocomplete-list li.component-type-row { + padding-left: 4px; +} +.f-autocomplete-list li.component-type-column { + padding-left: 8px; +} +.f-autocomplete-list li.component-type-field { + padding-left: 16px; +} +.f-autocomplete-list li.active-option.component-type-stage { + background-color: #4F46E5; + color: #f9fafb; +} +.f-autocomplete-list li.active-option.component-type-row { + background-color: rgb(229, 70, 188.2); + color: #f9fafb; +} +.f-autocomplete-list li.active-option.component-type-column { + background-color: rgb(229, 142.6, 70); + color: #111827; +} +.f-autocomplete-list li.active-option.component-type-field { + background-color: rgb(124.6, 229, 70); + color: #111827; +} +.f-autocomplete-list li.active-option.component-type-option { + background-color: rgb(70, 229, 206.2); + color: #111827; +} +.f-autocomplete-list li:hover.component-type-stage { + background-color: rgb(185.3507109005, 181.7890995261, 244.7109004739); + color: #111827; +} +.f-autocomplete-list li:hover.component-type-row { + background-color: rgb(244.7109004739, 181.7890995261, 228.56492891); + color: #111827; +} +.f-autocomplete-list li:hover.component-type-column { + background-color: rgb(244.7109004739, 210.5194312796, 181.7890995261); + color: #111827; +} +.f-autocomplete-list li:hover.component-type-field { + background-color: rgb(203.3962085308, 244.7109004739, 181.7890995261); + color: #111827; +} +.f-autocomplete-list li:hover.component-type-option { + background-color: rgb(181.7890995261, 244.7109004739, 235.6881516588); + color: #111827; +} +.f-autocomplete-list li:has(.options-list):hover .options-list { + display: block; +} +.f-autocomplete-list .component-type { + color: #666666; + font-style: italic; + font-size: 0.75em; +} +.f-autocomplete-list.options-list { + left: 100%; + top: 0; + width: 75%; + min-height: 100%; +} +.f-autocomplete-list.options-list li { + display: block; +} + +.formeo-panels-wrap h5 { + margin: 0; + padding: 0.55em 0; + color: #666666; + font-weight: normal; + display: inline-block; + width: 100%; +} +.formeo-panels-wrap nav { + position: relative; + padding: 0; + overflow: hidden; +} +.formeo-panels-wrap nav button { + position: absolute; + width: 24px; + color: #000; + height: 100%; + padding: 0; + line-height: 0; + z-index: 1; +} +.formeo-panels-wrap nav button:focus { + outline: none; + border: 1px solid rgb(102, 175, 233); + box-shadow: none; +} +.formeo-panels-wrap nav button .svg-icon { + width: 20px; + height: 20px; +} +.formeo-panels-wrap nav button.next-group { + right: 0; + top: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.formeo-panels-wrap nav button.prev-group { + left: 0; + top: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.formeo-panels-wrap ul { + margin: 0; + padding: 0; + list-style: none; +} +.formeo-panels-wrap .f-panel { + height: 0; + vertical-align: top; + display: inline-block; + width: 100%; + flex-direction: column; + flex: 1 0 100%; +} +.formeo-panels-wrap .f-panel > li:last-child { + border-radius: 0 0 4px 4px; +} +.formeo-panels-wrap .panels { + white-space: nowrap; + transition-property: height; + transition-duration: 150ms; + transition-timing-function: ease-in-out; + will-change: transform; + flex-direction: row; +} +.formeo-panels-wrap .panel-labels { + height: 100%; + background: #fff; + overflow: hidden; + text-align: center; + white-space: nowrap; +} +.formeo-panels-wrap .active-panel { + height: auto; +} + +.tabbed-panels .panel-nav { + height: auto; +} +.tabbed-panels .panel-nav button { + display: none; +} +.tabbed-panels .panel-labels div { + flex-direction: row; + justify-content: flex-start; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; + display: flex; +} +.tabbed-panels .panel-labels h5 { + flex-direction: column; + flex: 1; + cursor: pointer; + background-color: #cccccc; + box-shadow: 0 -1px 8px #999999 inset; +} +.tabbed-panels .panel-labels h5.active-tab { + color: #000; + box-shadow: none; + background-color: #fff; +} + +.formeo-dialog { + border: none; + border-radius: 8px; + padding: 0; + max-width: min(90vw, 500px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + position: absolute; + margin: 0; +} +.formeo-dialog.dialog-position-top { + left: 50%; + top: 20vh; + transform: translateX(-50%); +} +.formeo-dialog.dialog-position-center { + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} +.formeo-dialog.dialog-position-trigger { + left: -9999px; + top: -9999px; +} +.formeo-dialog::backdrop { + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + animation: backdrop-fade-in 266ms ease-out; +} +.formeo-dialog .dialog-form { + display: flex; + flex-direction: column; + gap: 8px; + padding: 16px; + min-width: 300px; +} +.formeo-dialog .dialog-title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: rgb(25.5, 25.5, 25.5); +} +.formeo-dialog .dialog-body { + display: flex; + flex-direction: column; + gap: 8px; +} +.formeo-dialog .dialog-body label { + display: flex; + flex-direction: column; + gap: 4px; + font-weight: 500; + font-size: 0.9rem; + color: #333333; +} +.formeo-dialog .dialog-body label input, +.formeo-dialog .dialog-body label textarea, +.formeo-dialog .dialog-body label select { + padding: 0.3em 0.6em; + border: 1px solid #cccccc; + border-radius: 4px; + font-size: 1rem; +} +.formeo-dialog .dialog-body label input:focus, +.formeo-dialog .dialog-body label textarea:focus, +.formeo-dialog .dialog-body label select:focus { + outline: 2px solid rgb(50, 93, 136); + outline-offset: 0; +} +.formeo-dialog .dialog-message { + margin: 0; + line-height: 1.6; + color: rgb(25.5, 25.5, 25.5); +} +.formeo-dialog .dialog-prompt-label .dialog-message { + margin-bottom: 4px; +} +.formeo-dialog .dialog-prompt-input { + width: 100%; +} +.formeo-dialog .dialog-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 4px; +} +.formeo-dialog .dialog-actions button { + min-width: 80px; +} +.formeo-dialog[open] { + animation: dialog-fade-in 266ms ease-out; +} +.formeo-dialog.config-item-dialog label { + display: flex; + flex-direction: row; + margin-bottom: 8px; +} + +@keyframes dialog-fade-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} +@keyframes dialog-fade-in-top { + from { + opacity: 0; + transform: translateX(-50%) scale(0.95); + } + to { + opacity: 1; + transform: translateX(-50%) scale(1); + } +} +@keyframes dialog-fade-in-center { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} +.dialog-position-top[open] { + animation: dialog-fade-in-top 266ms ease-out; +} + +.dialog-position-center[open] { + animation: dialog-fade-in-center 266ms ease-out; +} + +@keyframes backdrop-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +#formeo-sprite { + display: none !important; + visibility: hidden !important; +} + +.formeo * { + box-sizing: inherit; + font-family: inherit; +} +.formeo { + /* + Mixins + */ +} +.formeo .pill-buttons > button { + border-radius: 50px; +} +.formeo hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid #cccccc; +} +.formeo .f-field-group { + flex-wrap: wrap; + margin-bottom: 12px; +} +.formeo .f-field-group:last-child { + margin-bottom: 0; +} +.formeo .f-field-group label + .badge { + margin-left: 8px; +} +.formeo .f-field-group > label { + display: inline-block; + margin-bottom: 4px; +} +.formeo .f-field-group button { + margin-right: 4px; +} +.formeo select { + font-size: 100%; + font-family: inherit; + height: 2.1em; + line-height: 1.5; + margin: 0; + border: 1px solid #cccccc; + border-radius: 4px; + background-color: #fff; + width: 100%; + padding: 0.3em 0.6em; + box-sizing: border-box; +} +.formeo select:focus { + border: 1px solid rgb(102, 175, 233); + outline: none; +} +.formeo select[multiple] { + height: auto; +} +.formeo input { + font-size: 100%; + font-family: inherit; + height: 2.1em; + line-height: 1.5; + margin: 0; + border: 1px solid #cccccc; + border-radius: 4px; + background-color: #fff; + width: 100%; + padding: 0.3em 0.6em; + box-sizing: border-box; +} +.formeo input:focus { + border: 1px solid rgb(102, 175, 233); + outline: none; +} +.formeo input[type=checkbox], .formeo input[type=radio] { + width: auto; + height: auto; + margin-right: 4px; +} +.formeo input[type=date] { + max-width: 280px; + display: block; +} +.formeo textarea { + font-size: 100%; + font-family: inherit; + height: 2.1em; + line-height: 1.5; + margin: 0; + border: 1px solid #cccccc; + border-radius: 4px; + background-color: #fff; + width: 100%; + padding: 0.3em 0.6em; + box-sizing: border-box; +} +.formeo textarea:focus { + border: 1px solid rgb(102, 175, 233); + outline: none; +} +.formeo textarea { + height: auto; +} +.formeo button { + border-radius: 4px; + border: 1px solid #666666; + color: #333333; + background-color: #fff; + padding: 4px 8px; +} +.formeo button:active { + transform: scale(0.97); +} +.formeo button:hover { + filter: brightness(0.9); +} +.formeo button { + line-height: 1.5em; +} +.formeo button.primary, .formeo button.success, .formeo button.warning, .formeo button.error { + color: #fff; +} +.formeo button.primary { + background-color: rgb(50, 93, 136); + border-color: rgb(36.2903225806, 67.5, 98.7096774194); +} +.formeo button.success { + background-color: rgb(147, 197, 75); + border-color: rgb(120.7142857143, 167.1428571429, 53.8571428571); +} +.formeo button.warning { + background-color: rgb(244, 124, 60); + border-color: rgb(239.4902912621, 92.1116504854, 13.5097087379); +} +.formeo button.error { + background-color: rgb(217, 83, 79); + border-color: rgb(201.4953271028, 48.0841121495, 43.5046728972); +} +.formeo button[disabled] { + background-color: #cccccc; + color: #fff; +} +.formeo button:focus { + border: 1px solid rgb(102, 175, 233); +} +.formeo button:hover, .formeo button:focus { + outline: 0 none; +} +.formeo .f-addon { + font-size: 100%; + font-family: inherit; + height: 2.1em; + line-height: 1.5; + margin: 0; + border: 1px solid #cccccc; + border-radius: 4px; + background-color: #fff; + width: 100%; + padding: 0.3em 0.6em; + box-sizing: border-box; +} +.formeo .f-addon:focus { + border: 1px solid rgb(102, 175, 233); + outline: none; +} +.formeo .f-addon { + width: auto; +} +.formeo .f-addon label { + margin: 1px 0 0 3px; +} +.formeo .f-addon:last-child { + margin-left: -1px; +} +.formeo .f-btn-group { + display: inline-flex; + vertical-align: middle; +} +.formeo .f-btn-group > button { + flex: 0 1 auto; +} +.formeo .f-btn-group > button:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.formeo .f-btn-group > button:last-child:not(:first-child):not(.dropdown-toggle) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.formeo .f-btn-group > button:first-child { + margin-left: 0; +} +.formeo .f-btn-group > button:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.formeo .f-btn-group button + button, +.formeo .f-btn-group button + .f-btn-group, +.formeo .f-btn-group .f-btn-group + button, +.formeo .f-btn-group .f-btn-group + .f-btn-group, +.formeo .f-btn-group .f-btn-group-vertical button + button, +.formeo .f-btn-group .f-btn-group-vertical button + .f-btn-group, +.formeo .f-btn-group .f-btn-group-vertical .f-btn-group + button, +.formeo .f-btn-group .f-btn-group-vertical .f-btn-group + .f-btn-group { + margin-left: -1px; +} +.formeo .f-input-group { + display: flex; + flex-direction: row; + justify-content: flex-start; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; + display: inline-flex; + vertical-align: bottom; +} +.formeo .f-input-group select { + appearance: none; +} +.formeo .f-input-group select::-ms-expand { + display: none; +} +.formeo .f-input-group select { + background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyNCIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDMyIj4KPHBhdGggZmlsbD0iIzQ0NCIgZD0iTTAgMTJsMTEuOTkyIDExLjk5MiAxMS45OTItMTEuOTkyaC0yMy45ODR6Ij48L3BhdGg+Cjwvc3ZnPgo="); + background-position: right 10px top 2px; + background-repeat: no-repeat; + background-size: 16px; +} +.formeo .f-input-group .f-addon, +.formeo .f-input-group input, +.formeo .f-input-group select { + flex: 0 1 auto; + border-radius: 0; +} +.formeo .f-input-group .f-addon:last-child, +.formeo .f-input-group input:last-child, +.formeo .f-input-group select:last-child { + border-right-width: 1px; + border-radius: 0 4px 4px 0; +} +.formeo .f-input-group .f-addon:first-child, +.formeo .f-input-group input:first-child, +.formeo .f-input-group select:first-child { + border-radius: 4px 0 0 4px; + border-left-width: 1px; +} +.formeo .f-input-group .f-addon:first-child:last-child, +.formeo .f-input-group input:first-child:last-child, +.formeo .f-input-group select:first-child:last-child { + border-radius: 4px; +} +.formeo .f-input-group .f-addon:focus + input, +.formeo .f-input-group .f-addon:focus + select, +.formeo .f-input-group input:focus + input, +.formeo .f-input-group input:focus + select, +.formeo .f-input-group select:focus + input, +.formeo .f-input-group select:focus + select { + border-left: 1px solid rgb(102, 175, 233); +} +.formeo .text-primary { + color: rgb(50, 93, 136); +} +.formeo .text-success { + color: rgb(147, 197, 75); +} +.formeo .text-warning { + color: rgb(244, 124, 60); +} +.formeo .text-error { + color: rgb(217, 83, 79); +} +.formeo small.f-help-text { + display: block; +} +.formeo label .f-tooltip, .formeo .prev-label .f-tooltip { + margin: 0 4px; +} +.formeo label .f-tooltip svg, .formeo .prev-label .f-tooltip svg { + width: 16px; + height: 16px; +} +.formeo.formeo-editor { + display: flex; + flex-direction: row; + text-align: left; + gap: 16px; + /* + Mixins + */ + /* + Animations + */ +} +@keyframes PLACEHOLDER { + 0% { + height: 1px; + } + 100% { + height: 15px; + } +} +@keyframes DRAG_GHOST { + 0% { + box-shadow: 0 0 0 0 #999999; + } + 100% { + box-shadow: 0 0 30px 0 #999999; + } +} +@keyframes EDIT_PULSE { + from, to { + border-color: rgb(102, 175, 233); + } + 50% { + border-color: color-mix(in srgb, rgb(102, 175, 233) 80%, white); + } +} +@keyframes COMPONENT_HIGHLIGHT_PULSE { + 0% { + box-shadow: 0 0 1px 8px rgb(153, 84, 187); + } + 100% { + box-shadow: 0 0 0 0 rgb(153, 84, 187); + } +} +@keyframes SLIDE_UP { + 0% { + transform: translateY(100%); + clip-path: inset(0 0 100% -20%); + } + 100% { + transform: translateY(0); + clip-path: inset(0 0 0 -20%); + } +} +.formeo.formeo-editor .component-tag { + display: none; + height: 24px; + flex-direction: row; + gap: 4px; + align-items: center; + position: absolute; + font-size: 0.8em; + padding: 0 8px; + left: 50%; + top: -24px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + background-color: #fff; + border-color: #cccccc; + border-style: solid; + border-width: 1px 1px 0 1px; +} +.formeo.formeo-editor .component-tag [class*=-handle-] { + width: 12px; + height: 12px; +} +.formeo.formeo-editor .children { + margin: 0; + padding: 0; + list-style: none; +} +.formeo.formeo-editor .group-actions { + display: flex; + transition: opacity 0.3s ease-in-out allow-discrete; + position: absolute; + top: 0; + line-height: 0; + z-index: 1; + align-items: center; + justify-content: center; + flex-direction: row; + border-radius: 8px; +} +.formeo.formeo-editor .group-actions .action-btn-wrap { + display: none; + align-items: center; + justify-content: center; + border-top-right-radius: 8px; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + transition: opacity 1s ease-in-out allow-discrete; +} +.formeo.formeo-editor .group-actions .action-btn-wrap .component-handle { + opacity: 0.65; +} +.formeo.formeo-editor .group-actions .action-btn-wrap button { + background-color: #fff; +} +.formeo.formeo-editor .group-actions button { + background-color: transparent; + width: 24px; + height: 24px; + padding: 6px; + border: 0 none; + line-height: 0; +} +.formeo.formeo-editor .group-actions button:focus { + border: 0 none; + outline: 0 none; + box-shadow: none; +} +.formeo.formeo-editor .group-actions .svg-icon { + width: 12px; + height: 12px; +} +.formeo.formeo-editor .last-field .group-actions button:last-child { + border-radius: 0; +} +.formeo.formeo-editor .column-editing-field .column-actions { + display: none; +} +.formeo.formeo-editor .group-config { + display: none; + padding: 0.5rem; +} +.formeo.formeo-editor .editing-row .column-actions { + display: none; +} +.formeo.formeo-editor .hovering > .children, +.formeo.formeo-editor .hovering > .field-preview, +.formeo.formeo-editor .hovering > .prev-label { + opacity: 0.65; +} +.formeo.formeo-editor .hovering .group-actions .f-i-handle { + display: none; +} +.formeo.formeo-editor { + /* + Mixins + */ +} +.formeo.formeo-editor .formeo-row > .children { + display: flex; + flex-direction: row; + justify-content: flex-start; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; + gap: 16px; + min-height: 32px; +} +.formeo.formeo-editor .formeo-row { + transition: background-color 125ms ease-in-out; + position: relative; + clear: both; + background-color: #fff; + box-shadow: 0 0 0 1px #cccccc; + padding: 16px; +} +.formeo.formeo-editor .formeo-row::after { + clear: both; +} +.formeo.formeo-editor .formeo-row.control-ghost { + padding: 10px; +} +.formeo.formeo-editor .formeo-row:first-child:not(.editing-row, .hovering-row) { + border-top-left-radius: 8px; +} +.formeo.formeo-editor .formeo-row:first-child { + border-top-right-radius: 8px; +} +.formeo.formeo-editor .formeo-row:last-child { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} +.formeo.formeo-editor .formeo-row:only-child { + border-radius: 8px; +} +.formeo.formeo-editor .formeo-row.resizing-columns .formeo-column { + transition: none; +} +.formeo.formeo-editor .formeo-row.empty::after { + left: 0; + transform: translate(8px, -50%); +} +.formeo.formeo-editor .formeo-row .layout-row-control { + display: none; +} +.formeo.formeo-editor .formeo-row .row-tag { + left: -1px; + border-color: rgb(229, 70, 188.2); +} +.formeo.formeo-editor .formeo-row:has(.hovering-column) .row-actions, +.formeo.formeo-editor .formeo-row:has(.hovering-column) .field-actions { + display: none; +} +.formeo.formeo-editor .formeo-row.sortable-ghost { + background-color: rgb(244.7109004739, 181.7890995261, 228.56492891); + box-shadow: 0 0 0 1px rgb(229, 70, 188.2); +} +.formeo.formeo-editor .formeo-row.sortable-ghost * { + opacity: 0; +} +.formeo.formeo-editor .formeo-row:has(.editing-field) .row-actions { + display: none; +} +.formeo.formeo-editor .formeo-row.highlight-component { + box-shadow: 0 0 4px 2px rgb(229, 70, 188.2); +} +.formeo.formeo-editor .row-moving { + background-color: rgb(244.7109004739, 181.7890995261, 228.56492891) !important; +} +.formeo.formeo-editor .row-moving .action-btn-wrap button { + background-color: rgb(244.7109004739, 181.7890995261, 228.56492891); +} +.formeo.formeo-editor .row-moving .row-tag { + background-color: rgb(244.7109004739, 181.7890995261, 228.56492891); +} +.formeo.formeo-editor .row-actions { + left: 0; +} +.formeo.formeo-editor .hovering-row .column-actions, +.formeo.formeo-editor .hovering-row .field-actions { + display: none; +} +.formeo.formeo-editor .hovering-row.editing-row::before { + border-right-width: 0; +} +.formeo.formeo-editor .editing-row { + display: block; +} +.formeo.formeo-editor .editing-row .row-edit { + display: block; +} +.formeo.formeo-editor .editing-row.hovering-row .formeo-column { + opacity: 1; +} +.formeo.formeo-editor .hovering-row, +.formeo.formeo-editor .editing-row { + box-shadow: 0 0 0 1px rgb(229, 70, 188.2); +} +.formeo.formeo-editor .hovering-row:first-child, +.formeo.formeo-editor .editing-row:first-child { + border-top-left-radius: 0; +} +.formeo.formeo-editor .hovering-row .row-handle, +.formeo.formeo-editor .editing-row .row-handle { + display: none; +} +.formeo.formeo-editor .hovering-row .row-tag, +.formeo.formeo-editor .editing-row .row-tag { + display: flex; +} +.formeo.formeo-editor .hovering-row .row-action-btn-wrap, +.formeo.formeo-editor .editing-row .row-action-btn-wrap { + display: flex; +} +.formeo.formeo-editor .hovering-row .row-action-btn-wrap button:last-child, +.formeo.formeo-editor .editing-row .row-action-btn-wrap button:last-child { + border-bottom-right-radius: 8px; +} +.formeo.formeo-editor .row-edit { + padding-top: 2rem; +} +.formeo.formeo-editor .input-group-addon label { + margin-bottom: 0; +} +.formeo.formeo-editor .formeo-column { + margin: 0; + padding: 0; + list-style: none; + transition: width 266ms; + position: relative; + flex-direction: column; + will-change: width; + max-width: 100%; +} +.formeo.formeo-editor .formeo-column > .children { + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 16px; +} +.formeo.formeo-editor .formeo-column .column-tag { + border-color: rgb(229, 142.6, 70); +} +.formeo.formeo-editor .formeo-column .column-tag, +.formeo.formeo-editor .formeo-column .column-actions { + transform: translateX(-50%); +} +.formeo.formeo-editor .formeo-column[class*=col-] { + padding: 0; +} +.formeo.formeo-editor .formeo-column:first-child { + border-bottom-left-radius: 8px; +} +.formeo.formeo-editor .formeo-column:last-child { + border-bottom-right-radius: 8px; +} +.formeo.formeo-editor .formeo-column:last-child .resize-x-handle { + display: none !important; +} +.formeo.formeo-editor .formeo-column:only-child { + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; +} +.formeo.formeo-editor .formeo-column:only-child .resize-x-handle { + display: none !important; +} +.formeo.formeo-editor .formeo-column .resize-x-handle { + display: none; + position: absolute; + right: -16px; + top: 0; + bottom: 0; + width: 16px; + z-index: 2; + cursor: ew-resize; +} +.formeo.formeo-editor .formeo-column .resize-x-handle::before { + width: 0; + right: 6px; + border: 1px dashed rgb(238.4265402844, 183.3516587678, 137.0734597156); + border-width: 0 2px; + display: block; + top: 0; + position: absolute; + height: 100%; + content: ""; +} +.formeo.formeo-editor .formeo-column .resize-x-handle svg { + fill: rgb(238.4265402844, 183.3516587678, 137.0734597156); + position: absolute; + right: 1px; + width: 14px; +} +.formeo.formeo-editor .formeo-column .resize-x-handle svg.f-i-triangle-down { + top: -14px; +} +.formeo.formeo-editor .formeo-column .resize-x-handle svg.f-i-triangle-up { + bottom: -14px; +} +.formeo.formeo-editor .formeo-column .resize-x-handle:hover::before { + border-color: rgb(229, 142.6, 70); +} +.formeo.formeo-editor .formeo-column .resize-x-handle:hover svg { + fill: rgb(229, 142.6, 70); +} +.formeo.formeo-editor .formeo-column.editing-column { + overflow: hidden; +} +.formeo.formeo-editor .formeo-column.editing-column .column-edit { + display: block; +} +.formeo.formeo-editor .formeo-column:has(.hovering-field) .column-actions { + display: none; +} +.formeo.formeo-editor .formeo-column.sortable-ghost { + background-color: rgb(244.7109004739, 210.5194312796, 181.7890995261); + box-shadow: 0 0 0 1px rgb(229, 142.6, 70); +} +.formeo.formeo-editor .formeo-column.sortable-ghost * { + opacity: 0; +} +.formeo.formeo-editor .formeo-column:has(.editing-field) .column-actions { + display: none; +} +.formeo.formeo-editor .formeo-column.highlight-component { + box-shadow: 0 0 4px 2px rgb(229, 142.6, 70); +} +.formeo.formeo-editor .editing-row .formeo-column, +.formeo.formeo-editor .editing-row .empty { + border-radius: 8px; + height: 60px; + background-color: rgb(229.5, 229.5, 229.5); +} +.formeo.formeo-editor .editing-row .formeo-column.empty, +.formeo.formeo-editor .editing-row .empty.empty { + min-height: 0; +} +.formeo.formeo-editor .editing-row .formeo-column .formeo-field, +.formeo.formeo-editor .editing-row .empty .formeo-field { + display: none; +} +.formeo.formeo-editor .editing-row .formeo-column .resize-x-handle, +.formeo.formeo-editor .editing-row .empty .resize-x-handle { + display: block; +} +.formeo.formeo-editor .editing-row .formeo-column::after, +.formeo.formeo-editor .editing-row .empty::after { + color: #333333 !important; + line-height: 1em; + opacity: 1; + font-size: 1.1em; + content: attr(data-col-width) !important; + display: block; + width: 100%; + text-align: center; + position: absolute; + left: 50%; + margin-top: 0; + top: 50%; + transform: translate(-50%, -50%); +} +.formeo.formeo-editor .editing-field-preview .column-actions { + display: none; +} +.formeo.formeo-editor .column-moving { + background-color: rgb(244.7109004739, 210.5194312796, 181.7890995261); +} +.formeo.formeo-editor .column-moving .action-btn-wrap button { + background-color: rgb(244.7109004739, 210.5194312796, 181.7890995261); +} +.formeo.formeo-editor .column-moving .column-tag { + background-color: rgb(244.7109004739, 210.5194312796, 181.7890995261); +} +.formeo.formeo-editor button.column-handle { + background-color: rgba(255, 255, 255, 0.9); +} +.formeo.formeo-editor .hovering-column .column-tag { + display: flex; +} +.formeo.formeo-editor .hovering-column .column-handle { + display: none; +} +.formeo.formeo-editor .hovering-column .column-action-btn-wrap { + display: flex; +} +.formeo.formeo-editor .hovering-column:first-child { + border-top-left-radius: 0; +} +.formeo.formeo-editor .hovering-column::after { + opacity: 0; +} +.formeo.formeo-editor .hovering-column, +.formeo.formeo-editor .editing-column { + box-shadow: 0 0 0 1px rgb(229, 142.6, 70); +} +.formeo.formeo-editor .column-actions { + padding: 0; + left: 50%; + transition: width 0.15s; +} +.formeo.formeo-editor .column-actions .f-i-handle { + transform: rotate(90deg); +} +.formeo.formeo-editor .formeo-field { + min-height: 26px; + position: relative; + list-style: none; + margin: 0; + will-change: box-shadow; +} +.formeo.formeo-editor .formeo-field.highlight-component { + box-shadow: 0 0 4px 2px rgb(124.6, 229, 70); +} +.formeo.formeo-editor .formeo-field:last-child { + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.formeo.formeo-editor .formeo-field.first-field { + border-top-right-radius: 4px; +} +.formeo.formeo-editor .formeo-field.first-field .field-actions { + border-top-right-radius: 4px; +} +.formeo.formeo-editor .formeo-field .prev-label { + min-height: 24px; + max-width: calc(100% - 24px); + margin-bottom: 4px; +} +.formeo.formeo-editor .formeo-field .prev-label input { + width: auto; +} +.formeo.formeo-editor .formeo-field [contenteditable] { + padding: 1px 2px; + user-select: text; + display: inline-block; + position: relative; + min-width: 24px; +} +.formeo.formeo-editor .formeo-field [contenteditable]::after { + content: ""; + width: 100%; + position: absolute; + bottom: 0; + left: 0; + border-bottom: 1px dashed #cccccc; +} +.formeo.formeo-editor .formeo-field [contenteditable]:focus { + border-radius: 4px; + border-bottom-color: transparent; + outline: none; + box-shadow: 0 0 0 1px rgb(102, 175, 233) inset; +} +.formeo.formeo-editor .formeo-field [contenteditable]:focus::after { + display: none; +} +.formeo.formeo-editor .formeo-field .form-check { + margin-left: 1.25em; +} +.formeo.formeo-editor .formeo-field .form-check-input:only-child { + position: absolute; +} +.formeo.formeo-editor .formeo-field.field-type-hidden { + border: 1px dashed #cccccc; +} +.formeo.formeo-editor .formeo-field.sortable-ghost { + background-color: rgb(203.3962085308, 244.7109004739, 181.7890995261); + box-shadow: 0 0 0 1px rgb(124.6, 229, 70); +} +.formeo.formeo-editor .formeo-field.sortable-ghost * { + opacity: 0; +} +.formeo.formeo-editor .field-tag { + right: -1px; + left: auto; + top: -24px; + border-color: rgb(124.6, 229, 70); + background-color: #fff; +} +.formeo.formeo-editor .editing-field, +.formeo.formeo-editor .hovering-field { + box-shadow: 0 0 0 1px rgb(124.6, 229, 70); +} +.formeo.formeo-editor .editing-field .field-tag, +.formeo.formeo-editor .hovering-field .field-tag { + display: flex; +} +.formeo.formeo-editor .editing-field .field-action-btn-wrap, +.formeo.formeo-editor .hovering-field .field-action-btn-wrap { + display: flex; +} +.formeo.formeo-editor .editing-field .field-handle, +.formeo.formeo-editor .hovering-field .field-handle { + display: none; +} +.formeo.formeo-editor .field-actions { + border-color: transparent; + border-width: 1px 1px 0 0; + border-style: solid; + right: 0; + text-align: right; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 0; +} +.formeo.formeo-editor .field-actions .action-btn-wrap { + flex-direction: row-reverse; +} +.formeo.formeo-editor .field-moving { + background-color: rgb(203.3962085308, 244.7109004739, 181.7890995261); +} +.formeo.formeo-editor .field-moving .action-btn-wrap button { + background-color: rgb(203.3962085308, 244.7109004739, 181.7890995261); +} +.formeo.formeo-editor .field-moving .field-tag { + background-color: rgb(203.3962085308, 244.7109004739, 181.7890995261); +} +.formeo.formeo-editor .editing-field { + z-index: 1; +} +.formeo.formeo-editor .editing-field-preview .field-actions { + display: none; +} +.formeo.formeo-editor .field-preview p { + white-space: normal; +} +.formeo.formeo-editor .field-preview .f-checkbox.highlight-component, +.formeo.formeo-editor .field-preview .f-radio.highlight-component { + box-shadow: 0 0 2px 1px rgb(70, 229, 206.2) inset; +} +.formeo.formeo-editor .component-edit { + display: none; + overflow: hidden; +} +.formeo.formeo-editor .component-edit label { + font-size: 0.825em; +} +.formeo.formeo-editor .component-edit .panel-nav { + margin-bottom: 0; + padding: 0; + overflow: hidden; +} +.formeo.formeo-editor .component-edit .panel-nav button { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.formeo.formeo-editor .component-edit .component-prop { + display: flex; + gap: 8px; +} +.formeo.formeo-editor .component-edit .prop-controls { + flex-shrink: 0; + align-items: center; + display: flex; +} +.formeo.formeo-editor .component-edit .prop-controls .prop-remove { + z-index: 3; +} +.formeo.formeo-editor .component-edit .prop-controls.conditions-prop-controls { + display: flex; + align-items: flex-start; + margin-left: 16px; + min-width: 16px; +} +.formeo.formeo-editor .component-edit .prop-controls.conditions-prop-controls .prop-remove { + margin-top: -1px; +} +.formeo.formeo-editor .component-edit .prop-controls button { + position: relative; +} +.formeo.formeo-editor .component-edit .prop-controls .svg-icon { + width: 10px; + height: 10px; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); +} +.formeo.formeo-editor .component-edit .prop-control { + width: 16px; + height: 16px; + right: 0; + bottom: 0; + position: absolute; + padding: 0; +} +.formeo.formeo-editor .component-edit .prop-control:nth-of-type(1) { + right: 0px; +} +.formeo.formeo-editor .component-edit .prop-control:nth-of-type(2) { + right: 24px; +} +.formeo.formeo-editor .component-edit .prop-control:nth-of-type(3) { + right: 48px; +} +.formeo.formeo-editor .component-edit .prop-control:nth-of-type(4) { + right: 72px; +} +.formeo.formeo-editor .component-edit .prop-control:first-child { + right: 0; +} +.formeo.formeo-editor :not(.control-count-1) .component-edit .prop-control:last-child { + border-radius: 4px 0 0 4px; + display: none; +} +.formeo.formeo-editor :not(.control-count-1) .component-edit .prop-control:first-child { + border-radius: 4px; +} +.formeo.formeo-editor .component-edit .prop-control:hover:first-child { + border-radius: 0 4px 4px 0; +} +.formeo.formeo-editor .component-edit .prop-control:hover:first-child:last-child { + border-radius: 4px; +} +.formeo.formeo-editor .component-edit .prop-control:hover:last-child { + display: inline-block; +} +.formeo.formeo-editor .component-edit .prop-control:last-child:first-child { + display: inline-block; + border-radius: 4px; + margin-right: 0; +} +.formeo.formeo-editor .component-edit .to-remove { + position: relative; +} +.formeo.formeo-editor .component-edit .to-remove::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(217, 83, 79, 0.1); + z-index: 2; +} +.formeo.formeo-editor .component-edit .prop-wrap { + position: relative; + margin-bottom: 16px; + list-style: none; + margin-left: 0; +} +.formeo.formeo-editor .component-edit .prop-wrap:first-child .conditions-prop-controls .prop-remove { + display: none; +} +.formeo.formeo-editor .component-edit .field-edit-group { + padding: 8px 8px 0; +} +.formeo.formeo-editor .component-edit [contenteditable] { + display: flex; + flex-direction: row; + justify-content: flex-start; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; +} +.formeo.formeo-editor .component-edit .prop-inputs, +.formeo.formeo-editor .component-edit .prop-controls { + transition: width 150ms, display 150ms; + will-change: width; +} +.formeo.formeo-editor .component-edit .prop-inputs .f-addon, +.formeo.formeo-editor .component-edit .prop-inputs input:not([type=checkbox]):not([type=radio]), +.formeo.formeo-editor .component-edit .prop-inputs select, +.formeo.formeo-editor .component-edit .prop-inputs textarea, +.formeo.formeo-editor .component-edit .prop-inputs [contenteditable], +.formeo.formeo-editor .component-edit .prop-controls .f-addon, +.formeo.formeo-editor .component-edit .prop-controls input:not([type=checkbox]):not([type=radio]), +.formeo.formeo-editor .component-edit .prop-controls select, +.formeo.formeo-editor .component-edit .prop-controls textarea, +.formeo.formeo-editor .component-edit .prop-controls [contenteditable] { + font-size: 0.825em; +} +.formeo.formeo-editor .component-edit .prop-inputs .f-addon:focus, +.formeo.formeo-editor .component-edit .prop-inputs input:not([type=checkbox]):not([type=radio]):focus, +.formeo.formeo-editor .component-edit .prop-inputs select:focus, +.formeo.formeo-editor .component-edit .prop-inputs textarea:focus, +.formeo.formeo-editor .component-edit .prop-inputs [contenteditable]:focus, +.formeo.formeo-editor .component-edit .prop-controls .f-addon:focus, +.formeo.formeo-editor .component-edit .prop-controls input:not([type=checkbox]):not([type=radio]):focus, +.formeo.formeo-editor .component-edit .prop-controls select:focus, +.formeo.formeo-editor .component-edit .prop-controls textarea:focus, +.formeo.formeo-editor .component-edit .prop-controls [contenteditable]:focus { + z-index: 1; +} +.formeo.formeo-editor .f-input-wrap .component-edit [disabled] { + display: none; +} +.formeo.formeo-editor .component-edit .f-input-wrap { + display: flex; + align-items: center; + flex: 1; +} +.formeo.formeo-editor .component-edit .f-input-wrap.disabled-prop { + display: none; +} +.formeo.formeo-editor .component-edit .f-input-wrap.locked-prop { + pointer-events: none; + opacity: 0.7; +} +.formeo.formeo-editor .component-edit .f-input-wrap.locked-prop select { + background-image: none; +} +.formeo.formeo-editor .component-edit .control-count-2 .prop-controls:hover { + width: 48px; +} +.formeo.formeo-editor .component-edit .control-count-2 .prop-controls:hover + .prop-inputs { + width: calc(100% - 24px); +} +.formeo.formeo-editor .component-edit.panel-count-1 .panel-nav { + border-bottom: 1px solid #999999; +} +.formeo.formeo-editor .component-edit.panel-count-1 .panel-nav button { + display: none; +} +.formeo.formeo-editor .component-edit.panel-count-1 .panel-labels { + background-color: transparent; +} +.formeo.formeo-editor .component-edit[popover] { + display: block; + width: 66.7%; + height: 66.7%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-shadow: 0 0 24px 1px #000; + border: 1px solid #000; + border-radius: 8px; + z-index: 100; +} +.formeo.formeo-editor .f-condition-row { + display: flex; + width: 100%; + position: relative; + border-bottom: 1px solid #cccccc; +} +.formeo.formeo-editor .f-condition-row > [class^=condition-] { + border-left: 1px solid #cccccc; +} +.formeo.formeo-editor .f-condition-row .manage-condition-type { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + right: -16px; + position: absolute; + box-shadow: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + z-index: 1; +} +.formeo.formeo-editor .f-condition-row .remove-condition-type { + top: 0; +} +.formeo.formeo-editor .f-condition-row .add-condition-type { + bottom: 0; + display: none; +} +.formeo.formeo-editor .f-condition-row:last-child .remove-condition-type { + border-bottom-right-radius: 0; +} +.formeo.formeo-editor .f-condition-row:last-child .add-condition-type { + display: flex; +} +.formeo.formeo-editor .f-condition-row:last-child:not(:first-child) .add-condition-type { + border-top-right-radius: 0; +} +.formeo.formeo-editor .f-condition-row input, +.formeo.formeo-editor .f-condition-row select, +.formeo.formeo-editor .f-condition-row [contenteditable] { + background-color: #fff; + border: 0 none; + border-radius: 0; +} +.formeo.formeo-editor .f-condition-row input:focus, +.formeo.formeo-editor .f-condition-row select:focus, +.formeo.formeo-editor .f-condition-row [contenteditable]:focus { + box-shadow: 0 0 0 1px rgb(102, 175, 233) inset; +} +.formeo.formeo-editor .f-condition-row div.condition-source, +.formeo.formeo-editor .f-condition-row div.condition-target { + position: relative; + border-left: 1px solid #cccccc; +} +.formeo.formeo-editor .f-condition-row .condition-logical, +.formeo.formeo-editor .f-condition-row .condition-source, +.formeo.formeo-editor .f-condition-row .condition-target, +.formeo.formeo-editor .f-condition-row .condition-sourceProperty, +.formeo.formeo-editor .f-condition-row .condition-targetProperty, +.formeo.formeo-editor .f-condition-row .condition-comparison, +.formeo.formeo-editor .f-condition-row .condition-assignment, +.formeo.formeo-editor .f-condition-row .condition-value { + flex: 1 !important; + max-width: 100%; + transition: max-width 266ms; + will-change: max-width; + min-height: 31px; +} +.formeo.formeo-editor .f-condition-row .condition-logical { + max-width: 64px; + text-transform: uppercase; +} +.formeo.formeo-editor .f-condition-row .condition-source input[type=text], +.formeo.formeo-editor .f-condition-row .condition-target input[type=text] { + min-height: 31px; +} +.formeo.formeo-editor .f-condition-row .hidden-property { + max-width: 0% !important; + padding: 0; + border: 0 none; + overflow: hidden; + box-shadow: none; + margin: 0; +} +.formeo.formeo-editor .f-condition-row.display-none { + display: none; +} +.formeo.formeo-editor .f-condition-row .hidden-option { + display: none; +} +.formeo.formeo-editor .type-conditions-wrap { + width: 100%; +} +.formeo.formeo-editor .then-condition-row:first-of-type .remove-condition-type { + display: none; +} +.formeo.formeo-editor .if-condition-row:first-of-type .remove-condition-type { + display: none; +} +.formeo.formeo-editor .if-condition-row .condition-target input { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; +} +.formeo.formeo-editor .condition-target input.f-autocomplete-display-field:placeholder-shown + span.clear-button, +.formeo.formeo-editor .condition-source input.f-autocomplete-display-field:placeholder-shown + span.clear-button { + display: none; +} +.formeo.formeo-editor .condition-target .clear-button, +.formeo.formeo-editor .condition-source .clear-button { + position: absolute; + right: 2px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + padding: 4px; + display: flex; + justify-content: center; + align-items: center; +} +.formeo.formeo-editor .condition-target .clear-button .svg-icon, +.formeo.formeo-editor .condition-source .clear-button .svg-icon { + width: 12px; + height: 12px; + border-radius: 50%; + padding: 1px; + border: 1px solid #333333; + fill: #333333; +} +.formeo.formeo-editor .condition-target .clear-button:hover .svg-icon, +.formeo.formeo-editor .condition-source .clear-button:hover .svg-icon { + border: 1px solid rgb(217, 83, 79); + fill: rgb(217, 83, 79); +} +.formeo.formeo-editor .condition-target .clear-button:active .svg-icon, +.formeo.formeo-editor .condition-source .clear-button:active .svg-icon { + background-color: rgb(217, 83, 79); + border: 1px solid rgb(201.4953271028, 48.0841121495, 43.5046728972); + fill: #fff; +} +.formeo.formeo-editor .conditions-prop-inputs { + border-top: 1px solid #cccccc; + border-right: 1px solid #cccccc; +} +.formeo.formeo-editor .conditions-prop-inputs label.condition-label { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + padding: 4px; + border-right: 0; + text-transform: uppercase; + color: #333333; + background-color: #fff; +} +.formeo.formeo-editor .conditions-prop-inputs label.condition-label.then-condition-label { + background-color: #666666; + color: #fff; +} +.formeo.formeo-editor .field-conditions-0 .conditions-prop-inputs { + border-top: 0 none; +} +.formeo.formeo-editor .edit-group { + margin: 0; + padding: 0; + list-style: none; +} +.formeo.formeo-editor .options-panel .prop-wrap { + margin-bottom: 8px; +} +.formeo.formeo-editor .options-panel .input-group-addon { + line-height: 0; +} +.formeo.formeo-editor .options-panel .prop-labels { + padding: 8px 34px 8px 8px; +} +.formeo.formeo-editor .options-panel .prop-labels .input-group-addon { + font-size: 12px; +} +.formeo.formeo-editor .options-panel .prop-labels label { + font-size: 12px; + width: 50%; + position: relative; + display: table-cell; +} +.formeo.formeo-editor .options-panel .prop-label-selected, +.formeo.formeo-editor .options-panel .prop-label-disabled { + width: 1% !important; + white-space: nowrap; + vertical-align: middle; + border: 0 none; + background-color: transparent; +} +.formeo.formeo-editor .panel-action-buttons { + display: flex; + justify-content: end; + gap: 8px; + padding: 0 8px 8px; +} +.formeo.formeo-editor .panel-action-buttons button { + display: flex; + align-items: center; + justify-content: center; + line-height: 24px; + gap: 4px; +} +.formeo.formeo-editor .panel-action-buttons .svg-icon { + height: 16px; +} +.formeo.formeo-editor .panel-action-buttons [class^=add-] { + float: right; +} +.formeo.formeo-editor .prop-inputs { + width: 100%; + align-items: center; + vertical-align: bottom; + display: flex; + flex-direction: row; + flex-grow: 1; +} +.formeo.formeo-editor .prop-inputs.conditions-prop-inputs { + flex-direction: column; +} +.formeo.formeo-editor .prop-inputs .f-field-group { + width: 100%; + margin-bottom: 0; + display: inline-flex; + align-items: center; +} +.formeo.formeo-editor .prop-inputs .f-addon { + display: flex; + align-items: center; + flex: 0; +} +.formeo.formeo-editor .prop-inputs .f-addon input { + margin: 0; +} +.formeo.formeo-editor .prop-inputs label { + padding-right: 1em; + margin-bottom: 0; +} +.formeo.formeo-editor .formeo-stage { + width: 73%; + box-sizing: border-box; + transition: width 250ms; + margin-right: 4px; + flex: 1 1 auto; + position: relative; + transition-property: background-color, border; + transition-duration: 266ms, 266ms; + border-width: 1px; + border-style: dashed; + border-color: transparent; + background-color: rgba(255, 255, 255, 0); + padding-left: 23px; + padding-bottom: 8px; + overflow: visible; + border-radius: 8px 0 8px 8px; +} +.formeo.formeo-editor .formeo-stage > .children { + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 16px; + min-height: 100%; +} +.formeo.formeo-editor .formeo-stage.removing-all-fields .formeo-row { + transition: margin-top 250ms ease-in; +} +.formeo.formeo-editor .formeo-stage > .formeo-field { + background-color: #fff; +} +.formeo.formeo-editor .formeo-stage { + margin: 0; + padding: 0; + list-style: none; +} +@media (max-width: 481px) { + .formeo.formeo-editor .formeo-stage { + width: calc(100% - 50px); + } +} +.formeo.formeo-editor .formeo-stage.highlight-component { + box-shadow: 0 0 4px 2px #4F46E5; +} +.formeo.formeo-editor .formeo-stage .hidden { + display: none; +} +.formeo.formeo-editor .formeo-stage.empty { + border-color: #999999; + background-color: rgba(255, 255, 255, 0.25); +} +.formeo.formeo-editor .formeo-stage.editing-stage, .formeo.formeo-editor .formeo-stage.hovering-stage, .formeo.formeo-editor .formeo-stage.hovering-stage.empty { + border-color: #4F46E5; + border-style: solid; +} +.formeo.formeo-editor .formeo-stage.editing-stage .stage-tag, .formeo.formeo-editor .formeo-stage.hovering-stage .stage-tag, .formeo.formeo-editor .formeo-stage.hovering-stage.empty .stage-tag { + display: flex; +} +.formeo.formeo-editor .formeo-stage.editing-stage .stage-action-btn-wrap, .formeo.formeo-editor .formeo-stage.hovering-stage .stage-action-btn-wrap, .formeo.formeo-editor .formeo-stage.hovering-stage.empty .stage-action-btn-wrap { + display: flex; + border-top-right-radius: 8px; +} +.formeo.formeo-editor .formeo-stage.editing-stage .stage-handle, .formeo.formeo-editor .formeo-stage.hovering-stage .stage-handle, .formeo.formeo-editor .formeo-stage.hovering-stage.empty .stage-handle { + display: none; +} +.formeo.formeo-editor .formeo-stage.editing-stage .formeo-row, .formeo.formeo-editor .formeo-stage.hovering-stage .formeo-row, .formeo.formeo-editor .formeo-stage.hovering-stage.empty .formeo-row { + box-shadow: none; +} +.formeo.formeo-editor .stage-tag { + border: 0 none; + border: inherit; + display: none; + left: auto; + overflow: hidden; + padding: 0 32px 2px 8px; + right: -1px; + transition: width 266ms ease-in-out; + width: auto; + border-bottom-width: 0; +} +.formeo.formeo-editor .stage-edit { + padding: 16px; + background-color: #fff; + margin-top: 0; + margin-bottom: 16px; + border-top-left-radius: 8px; +} +.formeo.formeo-editor .stage-edit .panel-nav { + border-bottom: 0 none !important; +} +.formeo.formeo-editor .stage-edit .conditions-prop-inputs { + border-top: 1px solid #cccccc; +} +.formeo.formeo-editor .stage-actions { + right: -1px; + top: -24px; + text-align: right; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 0; + border-width: 0; +} +.formeo.formeo-editor .stage-actions .edit-toggle, +.formeo.formeo-editor .stage-actions .edit-toggle:focus { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-right: 1px solid #4F46E5; + border-top-left-radius: 0; + border-top-right-radius: 8px; + border-top: 1px solid #4F46E5; +} +.formeo.formeo-editor .editing-stage .formeo-settings { + display: block; +} +.formeo.formeo-editor .editing-stage .formeo-stage { + display: none; +} +.formeo.formeo-editor .f-field-group { + margin-bottom: 0; +} +.formeo.formeo-editor [class$=empty][class*=editing-]::after { + opacity: 0; +} +.formeo.formeo-editor [class$=empty]::after { + opacity: 1; + font-size: 24px; + position: absolute; + top: 50%; + left: 50%; + color: #999999; + transition: opacity 200ms ease-in-out; + will-change: opacity; + text-align: center; + transform: translate(-50%, -50%); + content: attr(data-hover-tag); +} +.formeo.formeo-editor .formeo-settings { + display: none; +} +.formeo.formeo-editor [dir=rtl] .f-btn-group { + display: inline-flex; + vertical-align: middle; +} +.formeo.formeo-editor [dir=rtl] .f-btn-group > button { + flex: 0 1 auto; +} +.formeo.formeo-editor [dir=rtl] .f-btn-group > button:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.formeo.formeo-editor [dir=rtl] .f-btn-group > button:last-child:not(:first-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; +} +.formeo.formeo-editor [dir=rtl] .f-btn-group > button:first-child { + margin-left: 0; +} +.formeo.formeo-editor [dir=rtl] .f-btn-group > button:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; +} +.formeo.formeo-editor [dir=rtl] .f-btn-group button + button, +.formeo.formeo-editor [dir=rtl] .f-btn-group button + .f-btn-group, +.formeo.formeo-editor [dir=rtl] .f-btn-group .f-btn-group + button, +.formeo.formeo-editor [dir=rtl] .f-btn-group .f-btn-group + .f-btn-group, +.formeo.formeo-editor [dir=rtl] .f-btn-group .f-btn-group-vertical button + button, +.formeo.formeo-editor [dir=rtl] .f-btn-group .f-btn-group-vertical button + .f-btn-group, +.formeo.formeo-editor [dir=rtl] .f-btn-group .f-btn-group-vertical .f-btn-group + button, +.formeo.formeo-editor [dir=rtl] .f-btn-group .f-btn-group-vertical .f-btn-group + .f-btn-group { + margin-right: -1px; +} +.formeo.formeo-editor [dir=rtl] .formeo-controls { + float: left; +} +.formeo.formeo-editor [dir=rtl] .formeo-controls .form-actions { + float: left; +} +.formeo.formeo-editor [dir=rtl] { + /* + Mixins + */ +} +.formeo.formeo-editor [dir=rtl] .formeo-stage-wrap { + float: right; +} +.formeo.formeo-editor [dir=rtl] .formeo-stage-wrap .formeo-stage { + padding-left: 4px; + padding-right: 23px; +} +.formeo.formeo-editor [dir=rtl] { + /* + Mixins + */ +} +.formeo.formeo-editor [dir=rtl] .formeo-row::before { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 0; + right: 0; + left: auto; +} +.formeo.formeo-editor [dir=rtl] .formeo-row:first-child { + border-top-left-radius: 8px; + border-top-right-radius: 0; +} +.formeo.formeo-editor [dir=rtl] .formeo-row:last-child { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} +.formeo.formeo-editor [dir=rtl] .formeo-row.hovering-row:first-child { + border-top-left-radius: 0; +} +.formeo.formeo-editor [dir=rtl] .formeo-row.hovering-row::before { + border-left-width: 1px; +} +.formeo.formeo-editor [dir=rtl] .formeo-row.empty::after { + left: 0; + right: auto; +} +.formeo.formeo-render { + /* + Mixins + */ +} +.formeo.formeo-render .formeo-row, +.formeo.formeo-render .formeo-column, +.formeo.formeo-render .formeo-field { + position: relative; +} +.formeo.formeo-render .f-input-group-wrap .formeo-row:first-of-type .remove-input-group { + display: none; +} +.formeo.formeo-render .f-input-group-wrap > fieldset { + position: relative; +} +.formeo.formeo-render .f-input-group-wrap > fieldset .remove-input-group { + top: 8px; +} +.formeo.formeo-render .formeo-row { + margin-bottom: 1em; + flex-direction: row; + justify-content: flex-start; + flex-wrap: nowrap; + align-content: stretch; + align-items: stretch; + display: flex; + border-radius: 8px; + outline: 0 solid rgba(217, 83, 79, 0.25); + transition: background-color 133ms, outline 133ms; + padding: 4px 0; +} +.formeo.formeo-render .formeo-row.will-remove { + border-radius: 2px; + background-color: rgba(217, 83, 79, 0.25); + outline: 6px solid rgba(217, 83, 79, 0.25); +} +.formeo.formeo-render .formeo-row:last-of-type { + margin-bottom: 0; +} +.formeo.formeo-render fieldset.formeo-row-wrap { + padding: 16px; +} +.formeo.formeo-render .formeo-row-wrap { + margin-bottom: 1em; + border-radius: 8px; +} +.formeo.formeo-render .formeo-row-wrap legend { + padding: 4px 8px; + border-style: groove; + border-width: 2px; + border-color: threedface; + border-radius: 8px; + font-size: 1.15em; +} +.formeo.formeo-render .formeo-row-wrap:last-child { + margin-bottom: 0; +} +.formeo.formeo-render .f-input-group { + position: relative; +} +.formeo.formeo-render .f-input-group:first-child .remove-input-group { + display: none; +} +.formeo.formeo-render .add-input-group { + float: right; + margin-top: 10px; +} +.formeo.formeo-render .remove-input-group { + position: absolute; + right: 0; + top: 0; + width: 24px; + height: 24px; + border: 0 none; + background: transparent; + outline: 0 none; + line-height: 0; + padding: 4px; +} +.formeo.formeo-render .remove-input-group:hover { + background-color: #fff; +} +.formeo.formeo-render .remove-input-group:hover .svg-icon { + fill: rgb(217, 83, 79); +} +.formeo.formeo-render .remove-input-group .svg-icon { + pointer-events: none; +} +.formeo.formeo-render .form-check-input:only-child { + position: absolute; +} +.formeo.formeo-render .svg-icon { + max-width: 100%; + max-height: 100%; +} +.formeo.formeo-render .formeo-column { + padding: 0 4px; + float: left; + max-width: none; + flex-direction: column; +} +.formeo.formeo-render .formeo-column:first-of-type { + padding-left: 0; +} +.formeo.formeo-render .formeo-column:last-of-type { + padding-right: 0; +} +.formeo.formeo-render .f-checkbox { + display: flex; + align-items: center; + gap: 4px; +} +.formeo button .svg-icon { + max-width: 100%; + max-height: 100%; +} + +/* + Mixins +*/ +.formeo-controls { + width: 26%; + max-width: 224px; + overflow: hidden; + container-type: inline-size; + container-name: controls; +} + +.formeo-controls.formeo-sticky { + position: sticky; + top: 0; + align-self: flex-start; +} + +.formeo-controls.pull-left .form-actions { + float: left; +} + +.formeo-controls .filtered-term { + background-color: #fff; + text-align: center; + border-radius: 4px 4px 0 0; + border: 1px solid #cccccc; + border-bottom: 0 none; + width: calc(100% - 2px); +} + +.formeo-controls .tabbed-panels nav { + padding: 0; +} + +.formeo-controls nav { + position: relative; + padding: 0 24px; + overflow: hidden; +} + +.formeo-controls nav h5 { + font-size: 13px; + line-height: 22px; +} + +.formeo-controls nav button { + position: absolute; + width: 24px; + color: #000; + height: calc(100% + 1px); + padding: 0; + line-height: 0; + margin: 0; + border-color: #cccccc; +} + +.formeo-controls nav button.next-group { + right: 0; + top: 0; + border-top-left-radius: 0; + border-top-right-radius: 8px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.formeo-controls nav button.prev-group { + border-top-left-radius: 8px; + border-bottom-left-radius: 0; + left: 0; +} + +.formeo-controls .formeo-panels-wrap { + font-size: 0.85em; + line-height: 1.8em; +} + +.formeo-controls .panel-labels { + border-top: 1px solid #cccccc; +} + +.formeo-controls .panel-count-1 { + border-bottom-left-radius: 0; +} + +.formeo-controls .panel-count-1 .panel-nav { + display: none; +} + +.formeo-controls .panel-count-1 .control-group li:first-child { + border-radius: 8px 8px 0 0; +} + +.formeo-controls ul { + margin: 0; + padding: 0; + list-style: none; +} + +.formeo-controls .control-group { + vertical-align: top; + display: inline-block; + width: 100%; + border-top: 1px solid #cccccc; +} + +.formeo-controls .control-group > li:first-child { + border-top-right-radius: 0; +} + +.formeo-controls .control-group > li:last-child { + border-radius: 0 0 8px 8px; +} + +.formeo-controls.filtered .panel-nav { + display: none; +} + +.formeo-controls.filtered .control-group { + display: block; +} + +.formeo-controls.filtered .control-group > li { + border-radius: 0; +} + +.formeo-controls.filtered .control-group:last-child > li:last-child { + border-radius: 0 0 0 4px; +} + +.formeo-controls .control-groups { + white-space: nowrap; +} + +.formeo-controls .control-group-labels { + height: 100%; + background: #fff; + overflow: hidden; +} + +.formeo-controls .control-group-labels div { + white-space: nowrap; +} + +.formeo-controls .control-group-labels h4 { + display: inline-block; + width: 100%; +} + +.formeo-controls .form-actions { + float: right; + margin-top: 4px; +} + +.formeo-controls .form-actions .svg-icon { + fill: #666666; + display: none; +} + +.formeo-controls .form-actions .svg-icon:hover { + fill: #000; +} + +.formeo-controls .form-actions .save-form:hover, +.formeo-controls .form-actions .clear-form:hover { + color: #fff; +} + +.formeo-controls .form-actions .save-form:hover { + background-color: rgb(50, 93, 136); +} + +.formeo-controls .form-actions .clear-form:hover { + background-color: rgb(217, 83, 79); +} + +@media (max-width: 481px) { + .formeo-controls { + width: 45px; + } + .formeo-controls .control-group { + text-indent: -9999px; + } + .formeo-controls .form-actions { + display: inline-block; + width: 100%; + position: relative; + vertical-align: middle; + float: none; + margin-top: 10px; + } + .formeo-controls .form-actions > button + button { + margin-top: -1px; + margin-left: 0; + } + .formeo-controls .form-actions > button { + max-width: 100%; + width: 100%; + float: none; + position: relative; + display: block; + } + .formeo-controls .form-actions > button:not(:first-child):not(:last-child) { + border-radius: 0; + } + .formeo-controls .form-actions > button:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + .formeo-controls .form-actions > button:last-child:not(:first-child) { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top-right-radius: 0; + border-top-left-radius: 0; + } + .formeo-controls .form-actions .control-icon { + display: inline-block; + } +} +.formeo-controls .field-control { + cursor: move; + list-style: none; + margin: -1px 0 0; + border: 1px solid #cccccc; + text-align: left; + background: #fff; + user-select: none; + overflow: hidden; +} + +.formeo-controls .field-control.control-moving { + border-radius: 8px; + animation: DRAG_GHOST 500ms forwards; +} + +.formeo-controls .field-control::before { + margin-right: 8px; + font-size: 16px; +} + +.formeo-controls .field-control:hover { + background-color: rgb(242.25, 242.25, 242.25); +} + +.formeo-controls .field-control button { + box-sizing: border-box; + font-size: 1em; + line-height: 1.8em; + display: flex; + height: 100%; + width: 100%; + background: transparent; + border: 0 none; + text-align: left; + padding: 8px; + border-radius: 0; +} + +.formeo-controls .field-control button:focus { + outline: 0 none; + background-color: rgb(242.25, 242.25, 242.25); + box-shadow: 0 0 0 1px rgb(102, 175, 233) inset; + border-radius: 0 !important; +} + +.formeo-controls .field-control button:active { + transform: none; +} + +.formeo-controls .field-control button:hover { + filter: none; +} + +.formeo-controls .field-control .control-icon { + margin-right: 8px; + text-align: center; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +[dir=rtl] .formeo-controls .field-control button { + text-align: right !important; +} + +[dir=rtl] .formeo-controls .field-control svg { + float: right !important; + margin: 0 0 0 8px !important; +} + +.formeo-controls .field-control .control-icon { + pointer-events: none; +} + +.formeo-controls .field-control .control-icon .f-i-hash { + padding: 2px; +} + +@container controls (max-width: 46px) { + .control-label { + display: none; + } + .form-actions button { + padding: 2px 0; + text-align: center; + } + .form-actions button:first-child { + border-top-right-radius: 4px !important; + border-top-left-radius: 4px !important; + border-bottom-right-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } + .form-actions button:last-child { + border-bottom-left-radius: 4px !important; + border-bottom-right-radius: 4px !important; + border-top-right-radius: 0 !important; + border-top-left-radius: 0 !important; + } +} +.field-control { + cursor: move; + list-style: none; + margin: -1px 0 0; + border: 1px solid #cccccc; + text-align: left; + background: #fff; + user-select: none; + overflow: hidden; +} +.field-control.control-moving { + border-radius: 8px; + animation: DRAG_GHOST 500ms forwards; +} +.field-control::before { + margin-right: 8px; + font-size: 16px; +} +.field-control:hover { + background-color: rgb(242.25, 242.25, 242.25); +} +.field-control button { + box-sizing: border-box; + font-size: 1em; + line-height: 1.8em; + display: flex; + height: 100%; + width: 100%; + background: transparent; + border: 0 none; + text-align: left; + padding: 8px; + border-radius: 0; +} +.field-control button:focus { + outline: 0 none; + background-color: rgb(242.25, 242.25, 242.25); + box-shadow: 0 0 0 1px rgb(102, 175, 233) inset; + border-radius: 0 !important; +} +.field-control button:active { + transform: none; +} +.field-control button:hover { + filter: none; +} +.field-control .control-icon { + margin-right: 8px; + text-align: center; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} +[dir=rtl] .field-control button { + text-align: right !important; +} +[dir=rtl] .field-control svg { + float: right !important; + margin: 0 0 0 8px !important; +} +.field-control .svg-icon { + pointer-events: none; +} \ No newline at end of file diff --git a/src/lib/sass/_render.scss b/src/lib/sass/_render.scss index fff9e7f0..43e79429 100644 --- a/src/lib/sass/_render.scss +++ b/src/lib/sass/_render.scss @@ -1,6 +1,6 @@ @use "base/mixins"; -@use "base/colors" as color; +@use "base/tokens" as t; @use "base/variables" as var; .formeo-row, @@ -33,7 +33,7 @@ align-items: stretch; display: flex; border-radius: var.$border-radius; - outline: 0 solid color.$remove-bg; + outline: 0 solid t.$remove-bg; transition: background-color var.$animation-speed-fast, outline var.$animation-speed-fast; @@ -41,8 +41,8 @@ &.will-remove { border-radius: 2px; - background-color: color.$remove-bg; - outline: 6px solid color.$remove-bg; + background-color: t.$remove-bg; + outline: 6px solid t.$remove-bg; } &:last-of-type { @@ -99,9 +99,9 @@ fieldset.formeo-row-wrap { padding: mixins.space(); &:hover { - background-color: color.$white; + background-color: t.$bg; .svg-icon { - fill: color.$brand-error; + fill: t.$danger; } } diff --git a/src/lib/sass/base/_animation.scss b/src/lib/sass/base/_animation.scss index f33b697c..e36aef5e 100644 --- a/src/lib/sass/base/_animation.scss +++ b/src/lib/sass/base/_animation.scss @@ -1,7 +1,6 @@ -@use 'sass:color' as sass-color; @use 'mixins'; -@use 'colors' as color; +@use 'tokens' as t; @use 'variables' as var; /* @@ -20,31 +19,31 @@ @keyframes DRAG_GHOST { 0% { - box-shadow: 0 0 0 0 color.$gray-light; + box-shadow: 0 0 0 0 t.$border-strong; } 100% { - box-shadow: 0 0 30px 0 color.$gray-light; + box-shadow: 0 0 30px 0 t.$border-strong; } } @keyframes EDIT_PULSE { from, to { - border-color: color.$input-focus; + border-color: t.$focus; } 50% { - border-color: sass-color.adjust(color.$input-focus, $lightness: 20%); + border-color: color-mix(in srgb, t.$focus 80%, white); // formeo-color-literal-ok: derived } } @keyframes COMPONENT_HIGHLIGHT_PULSE { 0% { - box-shadow: 0 0 1px mixins.space(1) color.$brand-info; + box-shadow: 0 0 1px mixins.space(1) t.$info; } 100% { - box-shadow: 0 0 0 0 color.$brand-info; + box-shadow: 0 0 0 0 t.$info; } } diff --git a/src/lib/sass/base/_bs.scss b/src/lib/sass/base/_bs.scss index c2bafc4a..ca1cc1e2 100644 --- a/src/lib/sass/base/_bs.scss +++ b/src/lib/sass/base/_bs.scss @@ -1,6 +1,6 @@ @use 'mixins'; -@use 'colors' as color; +@use 'tokens' as t; @use 'variables' as var; .pill-buttons>button { @@ -11,7 +11,7 @@ hr { margin-top: 1rem; margin-bottom: 1rem; border: 0; - border-top: 1px solid color.$gray-lighter; + border-top: 1px solid t.$border; } .f-field-group { @@ -169,7 +169,7 @@ button { +input, +select { - border-left: 1px solid color.$input-focus; + border-left: 1px solid t.$focus; } } } @@ -177,19 +177,19 @@ button { .text- { &primary { - color: color.$brand-primary; + color: t.$primary; } &success { - color: color.$brand-success; + color: t.$success; } &warning { - color: color.$brand-warning; + color: t.$warning; } &error { - color: color.$brand-error; + color: t.$danger; } } diff --git a/src/lib/sass/base/_icons.scss b/src/lib/sass/base/_icons.scss index 7f663ce1..f3843496 100644 --- a/src/lib/sass/base/_icons.scss +++ b/src/lib/sass/base/_icons.scss @@ -1,4 +1,4 @@ -@use 'colors' as color; +@use 'tokens' as t; @use 'variables' as var; @@ -9,47 +9,52 @@ pointer-events: none; } +:where(.svg-icon) { + fill: t.$icon; + color: t.$icon; +} + .f-i-remove { &:hover { - fill: color.$brand-error; + fill: t.$danger; } } button[class*='-move'] { &:hover { - background-color: color.$brand-info !important; + background-color: t.$info !important; .svg-icon { - fill: color.$white; + fill: t.$on-accent; } } } button[class*='-remove'] { &:hover { - background-color: color.$brand-error !important; + background-color: t.$danger !important; .svg-icon { - fill: color.$white; + fill: t.$on-accent; } } } button[class*='-clone'] { &:hover { - background-color: color.$brand-success !important; + background-color: t.$success !important; .svg-icon { - fill: color.$white; + fill: t.$on-accent; } } } .edit-toggle { &:hover { - background-color: color.$brand-primary !important; + background-color: t.$primary !important; .svg-icon { - fill: color.$white; + fill: t.$on-accent; } } } diff --git a/src/lib/sass/base/_mixins.scss b/src/lib/sass/base/_mixins.scss index b4056549..f4f8f258 100644 --- a/src/lib/sass/base/_mixins.scss +++ b/src/lib/sass/base/_mixins.scss @@ -1,5 +1,4 @@ -@use "sass:color" as sass-color; -@use "colors" as color; +@use "tokens" as t; @use "variables" as var; /* @@ -95,13 +94,13 @@ margin: 0; border: var.$input-border; border-radius: var.$input-border-radius; - background-color: color.$white; + background-color: t.$bg; width: 100%; padding: var.$input-padding; box-sizing: border-box; &:focus { - border: 1px solid color.$input-focus; + border: 1px solid t.$focus; outline: none; } } @@ -116,9 +115,9 @@ cursor: move; list-style: none; margin: -1px 0 0; - border: 1px solid color.$gray-lighter; + border: 1px solid t.$border; text-align: left; - background: color.$white; + background: t.$bg; user-select: none; overflow: hidden; @@ -133,7 +132,7 @@ } &:hover { - background-color: sass-color.adjust(color.$white, $lightness: -5%); + background-color: t.$bg-hover; } button { @@ -151,8 +150,8 @@ &:focus { outline: 0 none; - background-color: sass-color.adjust(color.$white, $lightness: -5%); - box-shadow: 0 0 0 1px color.$input-focus inset; + background-color: t.$bg-hover; + box-shadow: 0 0 0 1px t.$focus inset; border-radius: 0 !important; } @@ -189,9 +188,9 @@ @mixin button-style { border-radius: var.$input-border-radius; - border: 1px solid color.$gray; - color: color.$gray-dark; - background-color: color.$white; + border: 1px solid t.$text-muted; + color: t.$text-secondary; + background-color: t.$bg; padding: space() space(1); &:active { @@ -208,36 +207,36 @@ &.success, &.warning, &.error { - color: color.$white; + color: t.$on-accent; } &.primary { - background-color: color.$brand-primary; - border-color: color.$brand-primary-dark; + background-color: t.$primary; + border-color: t.$primary-dark; } &.success { - background-color: color.$brand-success; - border-color: color.$brand-success-dark; + background-color: t.$success; + border-color: t.$success-dark; } &.warning { - background-color: color.$brand-warning; - border-color: color.$brand-warning-dark; + background-color: t.$warning; + border-color: t.$warning-dark; } &.error { - background-color: color.$brand-error; - border-color: color.$brand-error-dark; + background-color: t.$danger; + border-color: t.$danger-dark; } &[disabled] { - background-color: color.$gray-lighter; - color: color.$white; + background-color: t.$border; + color: t.$on-accent; } &:focus { - border: 1px solid color.$input-focus; + border: 1px solid t.$focus; } &:hover, diff --git a/src/lib/sass/base/_properties.scss b/src/lib/sass/base/_properties.scss new file mode 100644 index 00000000..dce7b69a --- /dev/null +++ b/src/lib/sass/base/_properties.scss @@ -0,0 +1,104 @@ +@use "sass:color" as sass-color; +@use "sass:list"; +@use "colors" as c; + +$_structural: ( + stage: (c.$stage-outline-color, c.$stage-outline-color-text, c.$stage-highlight-color, c.$stage-highlight-color-text), + row: (c.$row-outline-color, c.$row-outline-color-text, c.$row-highlight-color, c.$row-highlight-color-text), + column: (c.$column-outline-color, c.$column-outline-color-text, c.$column-highlight-color, c.$column-highlight-color-text), + field: (c.$field-outline-color, c.$field-outline-color-text, c.$field-highlight-color, c.$field-highlight-color-text), + option: (c.$option-outline-color, c.$option-outline-color-text, c.$option-highlight-color, c.$option-highlight-color-text), +); + +:where(:root) { + --formeo-bg: #{c.$white}; + --formeo-bg-hover: #{sass-color.adjust(c.$white, $lightness: -5%)}; + --formeo-surface-muted: #{c.$gray-lightest}; + --formeo-stage-bg: #{c.$stage-background}; + --formeo-stage-shadow: #{c.$shadow}; + --formeo-overlay: #{rgba(255, 255, 255, 0.9)}; + + --formeo-text: #{c.$black}; + --formeo-text-strong: #{c.$gray-darker}; + --formeo-text-secondary: #{c.$gray-dark}; + --formeo-text-muted: #{c.$gray}; + --formeo-text-subtle: #{c.$gray-light}; + --formeo-on-accent: #{c.$white}; + --formeo-icon: #{c.$black}; + + --formeo-border: #{c.$gray-lighter}; + --formeo-border-strong: #{c.$gray-light}; + --formeo-focus: #{c.$input-focus}; + + --formeo-primary: #{c.$brand-primary}; + --formeo-primary-dark: #{c.$brand-primary-dark}; + --formeo-success: #{c.$brand-success}; + --formeo-success-dark: #{c.$brand-success-dark}; + --formeo-warning: #{c.$brand-warning}; + --formeo-warning-dark: #{c.$brand-warning-dark}; + --formeo-danger: #{c.$brand-error}; + --formeo-danger-dark: #{c.$brand-error-dark}; + --formeo-danger-subtle: #{rgba(c.$brand-error, 0.1)}; + --formeo-info: #{c.$brand-info}; + --formeo-remove-bg: #{c.$remove-bg}; + --formeo-column-outline-soft: #{sass-color.adjust(c.$column-outline-color, $lightness: 15%)}; + + @each $name, $values in $_structural { + --formeo-#{$name}-outline: #{list.nth($values, 1)}; + --formeo-#{$name}-outline-text: #{list.nth($values, 2)}; + --formeo-#{$name}-highlight: #{list.nth($values, 3)}; + --formeo-#{$name}-highlight-text: #{list.nth($values, 4)}; + } +} + +:where(.formeo-dark) { + color-scheme: dark; + --formeo-bg: #1f1f23; + --formeo-bg-hover: #2a2a2f; + --formeo-surface-muted: #26262b; + --formeo-stage-bg: rgba(0, 0, 0, 0); + --formeo-stage-shadow: rgba(0, 0, 0, 0.25); + --formeo-overlay: rgba(31, 31, 35, 0.9); + + --formeo-text: #f4f4f5; + --formeo-text-strong: #e4e4e7; + --formeo-text-secondary: #d4d4d8; + --formeo-text-muted: #a1a1aa; + --formeo-text-subtle: #71717a; + --formeo-icon: #e4e4e7; + + --formeo-border: #3f3f46; + --formeo-border-strong: #52525b; + + --formeo-danger-subtle: #{rgba(c.$brand-error, 0.2)}; + --formeo-remove-bg: #{rgba(c.$brand-error, 0.35)}; + + @each $name, $values in $_structural { + --formeo-#{$name}-highlight: #{rgba(list.nth($values, 1), 0.25)}; + --formeo-#{$name}-highlight-text: #f4f4f5; + } +} + +// Formeo containers inherit the host's text color by default; the dark preset must set its own. +:where(.formeo-dark) :where(.formeo, .formeo-controls, .formeo-dialog), +:where(.formeo-dark):where(.formeo, .formeo-controls, .formeo-dialog) { + color: var(--formeo-text); +} + +// White on --formeo-text-muted is too faint in dark; outranks the .formeo.formeo-editor rule. +.formeo-dark .formeo.formeo-editor .conditions-prop-inputs label.condition-label.then-condition-label, +.formeo-dark.formeo.formeo-editor .conditions-prop-inputs label.condition-label.then-condition-label { + background-color: var(--formeo-border-strong); +} + +// Dialogs otherwise keep the UA Canvas colors, which ignore the dark palette. +:where(.formeo-dark) :where(.formeo-dialog), +:where(.formeo-dark):where(.formeo-dialog) { + background-color: var(--formeo-bg); + color: var(--formeo-text); +} + +// The field being edited sits above its neighbours; give it the surface color. Zero specificity so highlights still win. +:where(.editing-field) { + background-color: var(--formeo-bg); +} diff --git a/src/lib/sass/base/_tokens.scss b/src/lib/sass/base/_tokens.scss new file mode 100644 index 00000000..9c6e570c --- /dev/null +++ b/src/lib/sass/base/_tokens.scss @@ -0,0 +1,53 @@ +// SCSS aliases for formeo's CSS custom properties. Component styles use these instead of colors.$*. +$bg: var(--formeo-bg); +$bg-hover: var(--formeo-bg-hover); +$surface-muted: var(--formeo-surface-muted); +$stage-bg: var(--formeo-stage-bg); +$stage-shadow: var(--formeo-stage-shadow); +$overlay: var(--formeo-overlay); + +$text: var(--formeo-text); +$text-strong: var(--formeo-text-strong); +$text-secondary: var(--formeo-text-secondary); +$text-muted: var(--formeo-text-muted); +$text-subtle: var(--formeo-text-subtle); +$on-accent: var(--formeo-on-accent); +$icon: var(--formeo-icon); + +$border: var(--formeo-border); +$border-strong: var(--formeo-border-strong); +$focus: var(--formeo-focus); + +$primary: var(--formeo-primary); +$primary-dark: var(--formeo-primary-dark); +$success: var(--formeo-success); +$success-dark: var(--formeo-success-dark); +$warning: var(--formeo-warning); +$warning-dark: var(--formeo-warning-dark); +$danger: var(--formeo-danger); +$danger-dark: var(--formeo-danger-dark); +$danger-subtle: var(--formeo-danger-subtle); +$info: var(--formeo-info); +$remove-bg: var(--formeo-remove-bg); +$column-outline-soft: var(--formeo-column-outline-soft); + +$stage-outline: var(--formeo-stage-outline); +$stage-outline-text: var(--formeo-stage-outline-text); +$stage-highlight: var(--formeo-stage-highlight); +$stage-highlight-text: var(--formeo-stage-highlight-text); +$row-outline: var(--formeo-row-outline); +$row-outline-text: var(--formeo-row-outline-text); +$row-highlight: var(--formeo-row-highlight); +$row-highlight-text: var(--formeo-row-highlight-text); +$column-outline: var(--formeo-column-outline); +$column-outline-text: var(--formeo-column-outline-text); +$column-highlight: var(--formeo-column-highlight); +$column-highlight-text: var(--formeo-column-highlight-text); +$field-outline: var(--formeo-field-outline); +$field-outline-text: var(--formeo-field-outline-text); +$field-highlight: var(--formeo-field-highlight); +$field-highlight-text: var(--formeo-field-highlight-text); +$option-outline: var(--formeo-option-outline); +$option-outline-text: var(--formeo-option-outline-text); +$option-highlight: var(--formeo-option-highlight); +$option-highlight-text: var(--formeo-option-highlight-text); diff --git a/src/lib/sass/base/_variables.scss b/src/lib/sass/base/_variables.scss index dedf8451..90459dd2 100644 --- a/src/lib/sass/base/_variables.scss +++ b/src/lib/sass/base/_variables.scss @@ -1,4 +1,4 @@ -@use "colors" as color; +@use "tokens" as t; // Sizing $base-space: 8px; @@ -11,7 +11,7 @@ $border-radius: $base-space; $input-border-radius: $half-space; $input-btn-border-width: 1px; $input-padding: 0.3em 0.6em; -$input-border: $input-btn-border-width solid color.$gray-lighter; +$input-border: $input-btn-border-width solid t.$border; $checkbox-character: "✔" !default; $animation-speed-base: 266ms; $animation-speed-slow: calc($animation-speed-base * 2); diff --git a/src/lib/sass/color-literals.test.mjs b/src/lib/sass/color-literals.test.mjs new file mode 100644 index 00000000..b719d52f --- /dev/null +++ b/src/lib/sass/color-literals.test.mjs @@ -0,0 +1,65 @@ +import { suite, test } from 'node:test' +import { checkGuardedFiles, findColorLiterals, GUARDED_FILES } from '../../../tools/check-color-literals.mjs' + +const flagged = text => findColorLiterals(text, 'sample.scss').length > 0 + +suite('color literal guard', () => { + test('guarded styles contain no color literals', t => { + t.assert.ok(GUARDED_FILES.some(file => file.startsWith('src/lib/sass/components/'))) + t.assert.deepStrictEqual(checkGuardedFiles(), []) + }) + + test('flags hex and every color function', t => { + for (const value of [ + '#fff', + '#a1a1aa80', + 'rgb(0 0 0)', + 'rgba(0, 0, 0, 0.5)', + 'hsl(0 0% 0%)', + 'hsla(0, 0%, 0%, 0.5)', + 'hwb(0 0% 0%)', + 'lab(50% 0 0)', + 'lch(50% 0 0)', + 'oklab(0.5 0 0)', + 'oklch(0.5 0 0)', + ]) { + t.assert.ok(flagged(` color: ${value};`), `${value} not flagged`) + } + }) + + test('flags the colors module however it is imported or referenced', t => { + t.assert.ok(flagged("@use '../base/colors' as c;")) + t.assert.ok(flagged('@use "colors" as palette;')) + t.assert.ok(flagged("@use 'colors';")) + t.assert.ok(flagged("@use 'colors' as *;")) + t.assert.ok(flagged(' border-color: color.$gray;')) + t.assert.ok(flagged(' border-color: colors.$gray;')) + const viaNamespace = findColorLiterals("@use 'colors' as c; // formeo-color-literal-ok: test\n fill: c.$x;", 's') + t.assert.deepStrictEqual(viaNamespace, ['s:2: fill: c.$x;']) + }) + + test('flags named colors in declaration values only', t => { + t.assert.ok(flagged(' border: 1px solid white;')) + t.assert.ok(flagged(' color: Black;')) + t.assert.ok(flagged(' background: rebeccapurple')) + t.assert.ok(flagged('$accent: tomato;')) + t.assert.ok(!flagged('.red, .white {')) + t.assert.ok(!flagged('li:first-child {')) + t.assert.ok(!flagged("@include mixins.theme('white');")) + t.assert.ok(!flagged(' white-space: nowrap;')) + t.assert.ok(!flagged(' border-color: t.$white;')) + t.assert.ok(!flagged(' background: transparent;')) + t.assert.ok(!flagged(' fill: currentcolor;')) + t.assert.ok(!flagged(' color: inherit;')) + }) + + test('allows marked lines, comments and token or variable references', t => { + t.assert.ok(!flagged(' box-shadow: 0 0 1px #000; // formeo-color-literal-ok: shadow')) + t.assert.ok(!flagged(' // border: 1px solid #000;')) + t.assert.ok(!flagged('/*\n color: white;\n*/')) + t.assert.ok(!flagged(' border-radius: var.$input-border-radius;')) + t.assert.ok(!flagged(' padding: mixins.space();')) + t.assert.ok(!flagged(' background-color: t.$bg;')) + t.assert.ok(!flagged("@use 'sass:color' as sass-color;")) + }) +}) diff --git a/src/lib/sass/components/_autocomplete.scss b/src/lib/sass/components/_autocomplete.scss index 1fec35fc..8c22f4aa 100644 --- a/src/lib/sass/components/_autocomplete.scss +++ b/src/lib/sass/components/_autocomplete.scss @@ -1,7 +1,7 @@ @use 'sass:color' as sass-color; @use '../base/mixins'; -@use '../base/colors' as color; +@use '../base/tokens' as t; @use '../base/variables' as var; .f-autocomplete { @@ -13,7 +13,7 @@ } .f-autocomplete-list { - background-color: color.$white; + background-color: t.$bg; display: none; list-style: none; padding: 0; @@ -21,13 +21,13 @@ z-index: 2; width: 100%; margin: 0 -1px; - border: 1px solid color.$gray-light; + border: 1px solid t.$border-strong; border-width: 1px 1px 0; - box-shadow: 0 1px mixins.space() 0 rgba(0, 0, 0, 0.5); + box-shadow: 0 1px mixins.space() 0 rgba(0, 0, 0, 0.5); // formeo-color-literal-ok: shadow li { position: relative; - border: 1px solid color.$gray-light; + border: 1px solid t.$border-strong; display: none; cursor: default; padding: mixins.space(); @@ -51,55 +51,55 @@ &.active-option { &.component-type-stage { - background-color: color.$stage-outline-color; - color: color.$stage-outline-color-text; + background-color: t.$stage-outline; + color: t.$stage-outline-text; } &.component-type-row { - background-color: color.$row-outline-color; - color: color.$row-outline-color-text; + background-color: t.$row-outline; + color: t.$row-outline-text; } &.component-type-column { - background-color: color.$column-outline-color; - color: color.$column-outline-color-text; + background-color: t.$column-outline; + color: t.$column-outline-text; } &.component-type-field { - background-color: color.$field-outline-color; - color: color.$field-outline-color-text; + background-color: t.$field-outline; + color: t.$field-outline-text; } &.component-type-option { - background-color: color.$option-outline-color; - color: color.$option-outline-color-text; + background-color: t.$option-outline; + color: t.$option-outline-text; } } &:hover { &.component-type-stage { - background-color: color.$stage-highlight-color; - color: color.$stage-highlight-color-text; + background-color: t.$stage-highlight; + color: t.$stage-highlight-text; } &.component-type-row { - background-color: color.$row-highlight-color; - color: color.$row-highlight-color-text; + background-color: t.$row-highlight; + color: t.$row-highlight-text; } &.component-type-column { - background-color: color.$column-highlight-color; - color: color.$column-highlight-color-text; + background-color: t.$column-highlight; + color: t.$column-highlight-text; } &.component-type-field { - background-color: color.$field-highlight-color; - color: color.$field-highlight-color-text; + background-color: t.$field-highlight; + color: t.$field-highlight-text; } &.component-type-option { - background-color: color.$option-highlight-color; - color: color.$option-highlight-color-text; + background-color: t.$option-highlight; + color: t.$option-highlight-text; } } @@ -109,7 +109,7 @@ } .component-type { - color: color.$gray; + color: t.$text-muted; font-style: italic; font-size: 0.75em; } diff --git a/src/lib/sass/components/_column.scss b/src/lib/sass/components/_column.scss index 4b5a2e75..67b89e0b 100644 --- a/src/lib/sass/components/_column.scss +++ b/src/lib/sass/components/_column.scss @@ -1,7 +1,6 @@ -@use "sass:color" as sass-color; @use "../base/mixins"; -@use "../base/colors" as color; +@use "../base/tokens" as t; @use "../base/variables" as var; .formeo-column { @@ -19,7 +18,7 @@ } .column-tag { - border-color: color.$column-outline-color; + border-color: t.$column-outline; } .column-tag, @@ -65,7 +64,7 @@ &::before { width: 0; right: 6px; - border: 1px dashed sass-color.adjust(color.$column-outline-color, $lightness: 15%); + border: 1px dashed t.$column-outline-soft; border-width: 0 2px; display: block; top: 0; @@ -75,7 +74,7 @@ } svg { - fill: sass-color.adjust(color.$column-outline-color, $lightness: 15%); + fill: t.$column-outline-soft; position: absolute; right: 1px; width: 14px; @@ -91,11 +90,11 @@ &:hover { &::before { - border-color: color.$column-outline-color; + border-color: t.$column-outline; } svg { - fill: color.$column-outline-color; + fill: t.$column-outline; } } } @@ -115,8 +114,8 @@ } &.sortable-ghost { - background-color: color.$column-highlight-color; - box-shadow: 0 0 0 1px color.$column-outline-color; + background-color: t.$column-highlight; + box-shadow: 0 0 0 1px t.$column-outline; * { opacity: 0; @@ -130,7 +129,7 @@ } &.highlight-component { - box-shadow: 0 0 mixins.space() 2px color.$column-outline-color; + box-shadow: 0 0 mixins.space() 2px t.$column-outline; } } @@ -141,7 +140,7 @@ .empty { border-radius: var.$border-radius; height: 60px; - background-color: color.$gray-lightest; + background-color: t.$surface-muted; &.empty { min-height: 0; @@ -156,7 +155,7 @@ } &::after { - color: color.$gray-dark !important; + color: t.$text-secondary !important; line-height: 1em; opacity: 1; font-size: 1.1em; @@ -180,21 +179,21 @@ } .column-moving { - background-color: color.$column-highlight-color; + background-color: t.$column-highlight; .action-btn-wrap { button { - background-color: color.$column-highlight-color; + background-color: t.$column-highlight; } } .column-tag { - background-color: color.$column-highlight-color; + background-color: t.$column-highlight; } } button.column-handle { - background-color: rgba(255, 255, 255, 0.9); + background-color: t.$overlay; } .hovering-column { @@ -221,7 +220,7 @@ button.column-handle { .hovering-column, .editing-column { - box-shadow: 0 0 0 1px color.$column-outline-color; + box-shadow: 0 0 0 1px t.$column-outline; } .column-actions { diff --git a/src/lib/sass/components/_controls.scss b/src/lib/sass/components/_controls.scss index d90df12b..f6064b26 100644 --- a/src/lib/sass/components/_controls.scss +++ b/src/lib/sass/components/_controls.scss @@ -1,6 +1,6 @@ @use "../base/mixins"; -@use "../base/colors" as color; +@use "../base/tokens" as t; @use "../base/variables" as var; .formeo-controls { @@ -23,10 +23,10 @@ } .filtered-term { - background-color: color.$white; + background-color: t.$bg; text-align: center; border-radius: var.$input-border-radius var.$input-border-radius 0 0; - border: 1px solid color.$gray-lighter; + border: 1px solid t.$border; border-bottom: 0 none; width: calc(100% - 2px); } @@ -50,12 +50,12 @@ button { position: absolute; width: var.$action-btn-width; - color: color.$black; + color: t.$text; height: calc(100% + 1px); padding: 0; line-height: 0; margin: 0; - border-color: color.$gray-lighter; + border-color: t.$border; &.next-group { right: 0; @@ -80,7 +80,7 @@ } .panel-labels { - border-top: 1px solid color.$gray-lighter; + border-top: 1px solid t.$border; // border-radius: $border-radius; } @@ -107,7 +107,7 @@ vertical-align: top; display: inline-block; width: 100%; - border-top: 1px solid color.$gray-lighter; + border-top: 1px solid t.$border; > li { &:first-child { @@ -148,7 +148,7 @@ .control-group-labels { height: 100%; - background: color.$white; + background: t.$bg; overflow: hidden; div { @@ -166,30 +166,30 @@ margin-top: mixins.space(); .svg-icon { - fill: color.$gray; + fill: t.$text-muted; display: none; &:hover { - fill: color.$black; + fill: t.$text; } } .save-form, .clear-form { &:hover { - color: color.$white; + color: t.$on-accent; } } .save-form { &:hover { - background-color: color.$brand-primary; + background-color: t.$primary; } } .clear-form { &:hover { - background-color: color.$brand-error; + background-color: t.$danger; } } } diff --git a/src/lib/sass/components/_dialog.scss b/src/lib/sass/components/_dialog.scss index 57c5a018..cf02570a 100644 --- a/src/lib/sass/components/_dialog.scss +++ b/src/lib/sass/components/_dialog.scss @@ -1,5 +1,5 @@ @use "../base/variables" as vars; -@use "../base/colors" as color; +@use "../base/tokens" as t; @use "../base/mixins" as mixins; .formeo-dialog { @@ -7,7 +7,7 @@ border-radius: vars.$border-radius; padding: 0; max-width: min(90vw, 500px); - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); // formeo-color-literal-ok: shadow position: absolute; margin: 0; @@ -34,7 +34,7 @@ } &::backdrop { - background-color: rgb(0 0 0 / 0.5); + background-color: rgb(0 0 0 / 0.5); // formeo-color-literal-ok: shadow backdrop-filter: blur(2px); animation: backdrop-fade-in vars.$animation-speed-base ease-out; } @@ -51,7 +51,7 @@ margin: 0; font-size: 1.25rem; font-weight: 600; - color: color.$gray-darker; + color: t.$text-strong; } .dialog-body { @@ -65,7 +65,7 @@ gap: vars.$half-space; font-weight: 500; font-size: 0.9rem; - color: color.$gray-dark; + color: t.$text-secondary; input, textarea, @@ -76,7 +76,7 @@ font-size: 1rem; &:focus { - outline: 2px solid color.$brand-primary; + outline: 2px solid t.$primary; outline-offset: 0; } } @@ -86,7 +86,7 @@ .dialog-message { margin: 0; line-height: 1.6; - color: color.$gray-darker; + color: t.$text-strong; } .dialog-prompt-label { diff --git a/src/lib/sass/components/_field-edit.scss b/src/lib/sass/components/_field-edit.scss index 7f34ff6f..f6990998 100644 --- a/src/lib/sass/components/_field-edit.scss +++ b/src/lib/sass/components/_field-edit.scss @@ -1,6 +1,6 @@ @use '../base/mixins'; -@use '../base/colors' as color; +@use '../base/tokens' as t; @use '../base/variables' as var; .component-edit { @@ -122,7 +122,7 @@ left: 0; width: 100%; height: 100%; - background-color: rgba(color.$brand-error, 0.1); + background-color: t.$danger-subtle; z-index: 2; } } @@ -215,7 +215,7 @@ &.panel-count-1 { .panel-nav { - border-bottom: 1px solid color.$gray-light; + border-bottom: 1px solid t.$border-strong; button { display: none; @@ -234,8 +234,8 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - box-shadow: 0 0 mixins.space(3) 1px #000; - border: 1px solid #000; + box-shadow: 0 0 mixins.space(3) 1px #000; // formeo-color-literal-ok: shadow + border: 1px solid t.$text; border-radius: mixins.space(1); z-index: 100; } @@ -245,10 +245,10 @@ display: flex; width: 100%; position: relative; - border-bottom: 1px solid color.$gray-lighter; + border-bottom: 1px solid t.$border; >[class^='condition-'] { - border-left: 1px solid color.$gray-lighter; + border-left: 1px solid t.$border; } .manage-condition-type { @@ -294,19 +294,19 @@ input, select, [contenteditable] { - background-color: color.$white; + background-color: t.$bg; border: 0 none; border-radius: 0; &:focus { - box-shadow: 0 0 0 1px color.$input-focus inset; + box-shadow: 0 0 0 1px t.$focus inset; } } div.condition-source, div.condition-target { position: relative; - border-left: 1px solid color.$gray-lighter; + border-left: 1px solid t.$border; } .condition-logical, @@ -407,30 +407,30 @@ height: mixins.space(1) + mixins.space(); border-radius: 50%; padding: 1px; - border: 1px solid color.$gray-dark; - fill: color.$gray-dark; + border: 1px solid t.$text-secondary; + fill: t.$text-secondary; } &:hover { .svg-icon { - border: 1px solid color.$brand-error; - fill: color.$brand-error; + border: 1px solid t.$danger; + fill: t.$danger; } } &:active { .svg-icon { - background-color: color.$brand-error; - border: 1px solid color.$brand-error-dark; - fill: color.$white; + background-color: t.$danger; + border: 1px solid t.$danger-dark; + fill: t.$on-accent; } } } } .conditions-prop-inputs { - border-top: 1px solid color.$gray-lighter; - border-right: 1px solid color.$gray-lighter; + border-top: 1px solid t.$border; + border-right: 1px solid t.$border; label.condition-label { display: inline-flex; @@ -439,12 +439,12 @@ padding: mixins.space(); border-right: 0; text-transform: uppercase; - color: color.$gray-dark; - background-color: color.$white; + color: t.$text-secondary; + background-color: t.$bg; &.then-condition-label { - background-color: color.$gray; - color: color.$white; + background-color: t.$text-muted; + color: t.$on-accent; } } } diff --git a/src/lib/sass/components/_field.scss b/src/lib/sass/components/_field.scss index 7a89baf1..a5e4406f 100644 --- a/src/lib/sass/components/_field.scss +++ b/src/lib/sass/components/_field.scss @@ -1,5 +1,5 @@ @use "../base/mixins"; -@use "../base/colors" as color; +@use "../base/tokens" as t; @use "../base/variables" as var; .formeo-field { @@ -10,7 +10,7 @@ will-change: box-shadow; &.highlight-component { - box-shadow: 0 0 mixins.space() 2px color.$field-outline-color; + box-shadow: 0 0 mixins.space() 2px t.$field-outline; } &:last-child { @@ -49,14 +49,14 @@ position: absolute; bottom: 0; left: 0; - border-bottom: 1px dashed color.$gray-lighter; + border-bottom: 1px dashed t.$border; } &:focus { border-radius: var.$input-border-radius; border-bottom-color: transparent; outline: none; - box-shadow: 0 0 0 1px color.$input-focus inset; + box-shadow: 0 0 0 1px t.$focus inset; &::after { display: none; @@ -73,12 +73,12 @@ } &.field-type-hidden { - border: 1px dashed color.$gray-lighter; + border: 1px dashed t.$border; } &.sortable-ghost { - background-color: color.$field-highlight-color; - box-shadow: 0 0 0 1px color.$field-outline-color; + background-color: t.$field-highlight; + box-shadow: 0 0 0 1px t.$field-outline; * { opacity: 0; @@ -91,15 +91,15 @@ left: auto; top: -(var.$icon-size); - border-color: color.$field-outline-color; - background-color: color.$white; + border-color: t.$field-outline; + background-color: t.$bg; } .editing-field, .hovering-field { - box-shadow: 0 0 0 1px color.$field-outline-color; + box-shadow: 0 0 0 1px t.$field-outline; .field-tag { display: flex; @@ -130,16 +130,16 @@ } .field-moving { - background-color: color.$field-highlight-color; + background-color: t.$field-highlight; .action-btn-wrap { button { - background-color: color.$field-highlight-color; + background-color: t.$field-highlight; } } .field-tag { - background-color: color.$field-highlight-color; + background-color: t.$field-highlight; } @@ -163,7 +163,7 @@ .f-checkbox, .f-radio { &.highlight-component { - box-shadow: 0 0 2px 1px color.$option-outline-color inset; + box-shadow: 0 0 2px 1px t.$option-outline inset; } } } diff --git a/src/lib/sass/components/_group-actions.scss b/src/lib/sass/components/_group-actions.scss index 7a57d59e..693c4333 100644 --- a/src/lib/sass/components/_group-actions.scss +++ b/src/lib/sass/components/_group-actions.scss @@ -1,4 +1,4 @@ -@use "../base/colors" as color; +@use "../base/tokens" as t; @use "../base/variables" as var; .group-actions { @@ -31,7 +31,7 @@ } button { - background-color: color.$white; + background-color: t.$bg; } } diff --git a/src/lib/sass/components/_panels.scss b/src/lib/sass/components/_panels.scss index 4fac5050..03e2686b 100644 --- a/src/lib/sass/components/_panels.scss +++ b/src/lib/sass/components/_panels.scss @@ -1,13 +1,13 @@ @use '../base/mixins'; -@use '../base/colors' as color; +@use '../base/tokens' as t; @use '../base/variables' as var; .formeo-panels-wrap { h5 { margin: 0; padding: 0.55em 0; - color: color.$gray; + color: t.$text-muted; font-weight: normal; display: inline-block; width: 100%; @@ -21,7 +21,7 @@ button { position: absolute; width: 24px; - color: color.$black; + color: t.$text; height: 100%; padding: 0; line-height: 0; @@ -29,7 +29,7 @@ &:focus { outline: none; - border: 1px solid color.$input-focus; + border: 1px solid t.$focus; box-shadow: none; } @@ -85,7 +85,7 @@ .panel-labels { height: 100%; - background: color.$white; + background: t.$bg; overflow: hidden; text-align: center; white-space: nowrap; @@ -119,13 +119,13 @@ flex-direction: column; flex: 1; cursor: pointer; - background-color: color.$gray-lighter; - box-shadow: 0 -1px mixins.space(1) color.$gray-light inset; + background-color: t.$border; + box-shadow: 0 -1px mixins.space(1) t.$border-strong inset; &.active-tab { - color: color.$black; + color: t.$text; box-shadow: none; - background-color: color.$white; + background-color: t.$bg; } } } diff --git a/src/lib/sass/components/_row.scss b/src/lib/sass/components/_row.scss index 0923d617..7f2bec9d 100644 --- a/src/lib/sass/components/_row.scss +++ b/src/lib/sass/components/_row.scss @@ -1,6 +1,6 @@ @use "../base/mixins"; -@use "../base/colors" as color; +@use "../base/tokens" as t; @use "../base/variables" as var; .formeo-row { @@ -13,8 +13,8 @@ transition: background-color 125ms ease-in-out; position: relative; clear: both; - background-color: color.$white; - box-shadow: 0 0 0 1px color.$gray-lighter; + background-color: t.$bg; + box-shadow: 0 0 0 1px t.$border; padding: mixins.space(2); &::after { @@ -61,7 +61,7 @@ .row-tag { left: -1px; - border-color: color.$row-outline-color; + border-color: t.$row-outline; } @@ -75,8 +75,8 @@ } &.sortable-ghost { - background-color: color.$row-highlight-color; - box-shadow: 0 0 0 1px color.$row-outline-color; + background-color: t.$row-highlight; + box-shadow: 0 0 0 1px t.$row-outline; * { opacity: 0; @@ -91,21 +91,21 @@ &.highlight-component { - box-shadow: 0 0 mixins.space() 2px color.$row-outline-color; + box-shadow: 0 0 mixins.space() 2px t.$row-outline; } } .row-moving { - background-color: color.$row-highlight-color !important; + background-color: t.$row-highlight !important; .action-btn-wrap { button { - background-color: color.$row-highlight-color; + background-color: t.$row-highlight; } } .row-tag { - background-color: color.$row-highlight-color; + background-color: t.$row-highlight; } @@ -145,7 +145,7 @@ .hovering-row, .editing-row { - box-shadow: 0 0 0 1px color.$row-outline-color; + box-shadow: 0 0 0 1px t.$row-outline; &:first-child { border-top-left-radius: 0; diff --git a/src/lib/sass/components/_stage.scss b/src/lib/sass/components/_stage.scss index 3e2ab6ff..d351e131 100644 --- a/src/lib/sass/components/_stage.scss +++ b/src/lib/sass/components/_stage.scss @@ -1,4 +1,4 @@ -@use '../base/colors' as color; +@use '../base/tokens' as t; @use '../base/variables' as var; @use 'group-actions'; @@ -21,7 +21,7 @@ border-width: 1px; border-style: dashed; border-color: transparent; - background-color: color.$stage-background; + background-color: t.$stage-bg; padding-left: var.$action-btn-width - 1; padding-bottom: mixins.space(1); overflow: visible; @@ -40,7 +40,7 @@ } &>.formeo-field { - background-color: color.$white; + background-color: t.$bg; } @include mixins.no-list-style; @@ -50,7 +50,7 @@ } &.highlight-component { - box-shadow: 0 0 mixins.space() 2px color.$stage-outline-color; + box-shadow: 0 0 mixins.space() 2px t.$stage-outline; } .hidden { @@ -58,14 +58,14 @@ } &.empty { - border-color: color.$gray-light; - background-color: color.$shadow; + border-color: t.$border-strong; + background-color: t.$stage-shadow; } &.editing-stage, &.hovering-stage, &.hovering-stage.empty { - border-color: color.$stage-outline-color; + border-color: t.$stage-outline; border-style: solid; .stage-tag { @@ -103,7 +103,7 @@ .stage-edit { padding: mixins.space(2); - background-color: color.$white; + background-color: t.$bg; margin-top: 0; margin-bottom: mixins.space(2); border-top-left-radius: mixins.space(1); @@ -113,7 +113,7 @@ } .conditions-prop-inputs { - border-top: 1px solid color.$gray-lighter; + border-top: 1px solid t.$border; } } @@ -129,10 +129,10 @@ .edit-toggle:focus { border-bottom-left-radius: 0; border-bottom-right-radius: 0; - border-right: 1px solid color.$stage-outline-color; + border-right: 1px solid t.$stage-outline; border-top-left-radius: 0; border-top-right-radius: mixins.space(1); - border-top: 1px solid color.$stage-outline-color; + border-top: 1px solid t.$stage-outline; } } @@ -163,7 +163,7 @@ position: absolute; top: 50%; left: 50%; - color: color.$gray-light; + color: t.$text-subtle; transition: opacity 200ms ease-in-out; will-change: opacity; text-align: center; diff --git a/src/lib/sass/components/component.scss b/src/lib/sass/components/component.scss index a3eaa1e2..ce1b21ac 100644 --- a/src/lib/sass/components/component.scss +++ b/src/lib/sass/components/component.scss @@ -1,5 +1,5 @@ @use "../base/mixins"; -@use "../base/colors" as color; +@use "../base/tokens" as t; @use "../base/variables" as var; @use "../base/animation"; @@ -18,8 +18,8 @@ top: -(var.$icon-size); border-top-left-radius: var.$border-radius; border-top-right-radius: var.$border-radius; - background-color: color.$white; - border-color: color.$gray-lighter; + background-color: t.$bg; + border-color: t.$border; border-style: solid; border-width: 1px 1px 0 1px; diff --git a/src/lib/sass/css-properties.test.mjs b/src/lib/sass/css-properties.test.mjs new file mode 100644 index 00000000..ef610433 --- /dev/null +++ b/src/lib/sass/css-properties.test.mjs @@ -0,0 +1,180 @@ +import { readFileSync } from 'node:fs' +import { suite, test } from 'node:test' +import { compileFormeoCss } from '../../../tools/compile-scss.mjs' +import { ALLOWED_ADDITIONS, resolveFormeoProperties } from '../../../tools/resolve-formeo-properties.mjs' + +const baseline = readFileSync(new URL('./__fixtures__/formeo-baseline.css', import.meta.url), 'utf8') + +const EXPECTED = [ + 'bg', + 'bg-hover', + 'surface-muted', + 'stage-bg', + 'stage-shadow', + 'overlay', + 'text', + 'text-strong', + 'text-secondary', + 'text-muted', + 'text-subtle', + 'on-accent', + 'icon', + 'border', + 'border-strong', + 'focus', + 'primary', + 'primary-dark', + 'success', + 'success-dark', + 'warning', + 'warning-dark', + 'danger', + 'danger-dark', + 'danger-subtle', + 'info', + 'remove-bg', + 'column-outline-soft', + ...['stage', 'row', 'column', 'field', 'option'].flatMap(s => [ + `${s}-outline`, + `${s}-outline-text`, + `${s}-highlight`, + `${s}-highlight-text`, + ]), +].map(n => `--formeo-${n}`) + +suite('formeo CSS custom properties', () => { + test('resolved CSS is identical to the pre-change baseline', t => { + const { css } = resolveFormeoProperties(compileFormeoCss()) + t.assert.strictEqual(css.trim(), baseline.trim()) + }) + + test('declares every expected property on :where(:root)', t => { + const { defaults } = resolveFormeoProperties(compileFormeoCss()) + t.assert.deepStrictEqual([...defaults.keys()].sort(), [...EXPECTED].sort()) + }) + + test('.formeo-dark preset overrides surfaces and text but keeps on-accent light', t => { + const { dark, defaults } = resolveFormeoProperties(compileFormeoCss()) + for (const name of ['--formeo-bg', '--formeo-text', '--formeo-border', '--formeo-icon']) { + t.assert.ok(dark.has(name), `${name} missing from dark preset`) + t.assert.notStrictEqual(dark.get(name), defaults.get(name)) + } + t.assert.ok(!dark.has('--formeo-on-accent'), 'on-accent must not be overridden') + for (const key of dark.keys()) t.assert.ok(defaults.has(key), `${key} has no default`) + }) + + test('every allowed addition matches exactly one compiled rule', t => { + const compiled = compileFormeoCss() + for (const re of ALLOWED_ADDITIONS) { + t.assert.strictEqual([...compiled.matchAll(re)].length, 1, `${re} must match exactly one rule`) + } + }) + + test(':where(.svg-icon) sets the icon fill and color', t => { + t.assert.strictEqual( + compiledRule(':where(.svg-icon) {'), + ':where(.svg-icon) {\n fill: var(--formeo-icon);\n color: var(--formeo-icon);\n}' + ) + }) + + test('.formeo-dark preset sets text color on formeo containers, nested or on the container itself', t => { + t.assert.strictEqual( + compiledRule(':where(.formeo-dark) :where(.formeo,'), + [ + ':where(.formeo-dark) :where(.formeo, .formeo-controls, .formeo-dialog),', + ':where(.formeo-dark):where(.formeo, .formeo-controls, .formeo-dialog) {', + ' color: var(--formeo-text);', + '}', + ].join('\n') + ) + }) + + test('.formeo-dark gives the THEN condition label a higher-contrast background', t => { + const label = '.formeo.formeo-editor .conditions-prop-inputs label.condition-label.then-condition-label' + t.assert.strictEqual( + compiledRule(`.formeo-dark ${label},`), + [ + `.formeo-dark ${label},`, + `.formeo-dark${label} {`, + ' background-color: var(--formeo-border-strong);', + '}', + ].join('\n') + ) + }) + + test('.formeo-dark gives dialogs the dark surface and text, nested or on the dialog itself', t => { + t.assert.strictEqual( + compiledRule(':where(.formeo-dark) :where(.formeo-dialog)'), + [ + ':where(.formeo-dark) :where(.formeo-dialog),', + ':where(.formeo-dark):where(.formeo-dialog) {', + ' background-color: var(--formeo-bg);', + ' color: var(--formeo-text);', + '}', + ].join('\n') + ) + }) + + test(':where(.editing-field) gives the edited field the surface background', t => { + t.assert.strictEqual( + compiledRule(':where(.editing-field) {'), + ':where(.editing-field) {\n background-color: var(--formeo-bg);\n}' + ) + }) + + test('allowed additions are stripped before the baseline comparison', t => { + const { css } = resolveFormeoProperties(compileFormeoCss()) + for (const fragment of ['formeo-dark', ':where(.svg-icon)', ':where(.editing-field)']) { + t.assert.ok(!css.includes(fragment), `${fragment} must be stripped before baseline comparison`) + } + }) +}) + +suite('resolveFormeoProperties purity', () => { + const root = ':where(:root) {\n --formeo-bg: #fff;\n}\n' + const dark = ':where(.formeo-dark) {\n color-scheme: dark;\n --formeo-bg: #000;\n}\n' + const use = '.a {\n color: var(--formeo-bg);\n}\n' + + test('resolves a well-formed stylesheet', t => { + const result = resolveFormeoProperties(root + dark + use) + t.assert.strictEqual(result.css, '.a {\n color: #fff;\n}\n') + t.assert.strictEqual(result.dark.get('--formeo-bg'), '#000') + }) + + test('rejects more than one :where(:root) block', t => { + t.assert.throws(() => resolveFormeoProperties(root + root + use), /at most one :where\(:root\) block, found 2/) + }) + + test('counts a :where(:root) block nested in an at-rule', t => { + const nested = `@media print {\n ${root}}\n` + t.assert.throws(() => resolveFormeoProperties(root + nested + use), /at most one :where\(:root\) block, found 2/) + }) + + test('rejects more than one :where(.formeo-dark) block', t => { + t.assert.throws( + () => resolveFormeoProperties(root + dark + dark), + /at most one :where\(\.formeo-dark\) block, found 2/ + ) + }) + + test('rejects non-property declarations in the root block', t => { + const impure = ':where(:root) {\n --formeo-bg: #fff;\n color: red;\n}\n' + t.assert.throws(() => resolveFormeoProperties(impure), /:where\(:root\) may only declare .*found: color: red/) + }) + + test('rejects anything but properties and color-scheme in the dark block', t => { + const impure = ':where(.formeo-dark) {\n color-scheme: dark;\n background: black;\n}\n' + t.assert.throws( + () => resolveFormeoProperties(root + impure), + /:where\(\.formeo-dark\) may only declare .*found: background: black/ + ) + }) +}) + +/** The single compiled rule starting with `start`, trimmed. */ +function compiledRule(start) { + const compiled = compileFormeoCss() + const at = compiled.indexOf(start) + if (at === -1 || compiled.indexOf(start, at + 1) !== -1) throw new Error(`expected one rule starting ${start}`) + return compiled.slice(at, compiled.indexOf('}', at) + 1) +} diff --git a/src/lib/sass/formeo.scss b/src/lib/sass/formeo.scss index 1681bec3..c194fd6c 100644 --- a/src/lib/sass/formeo.scss +++ b/src/lib/sass/formeo.scss @@ -1,3 +1,4 @@ +@use "base/properties"; @use "sass:meta"; @use "base/variables"; @use "base/mixins"; diff --git a/tools/check-color-literals.mjs b/tools/check-color-literals.mjs new file mode 100644 index 00000000..a97c4f87 --- /dev/null +++ b/tools/check-color-literals.mjs @@ -0,0 +1,226 @@ +import { globSync, readFileSync } from 'node:fs' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const OK = '// formeo-color-literal-ok' +const root = fileURLToPath(new URL('../', import.meta.url)) + +/** Files that must take every color from tokens.$* (paths relative to the repo root). */ +export const GUARDED_FILES = [ + ...globSync('src/lib/sass/components/**/*.scss', { cwd: root }).sort(), + 'src/lib/sass/_render.scss', + 'src/lib/sass/base/_bs.scss', + 'src/lib/sass/base/_icons.scss', + 'src/lib/sass/base/_mixins.scss', + 'src/lib/sass/base/_variables.scss', + 'src/lib/sass/base/_animation.scss', +] + +/** Every CSS named color (CSS Color 4). transparent, currentcolor and the global keywords are allowed. */ +export const NAMED_COLORS = [ + 'aliceblue', + 'antiquewhite', + 'aqua', + 'aquamarine', + 'azure', + 'beige', + 'bisque', + 'black', + 'blanchedalmond', + 'blue', + 'blueviolet', + 'brown', + 'burlywood', + 'cadetblue', + 'chartreuse', + 'chocolate', + 'coral', + 'cornflowerblue', + 'cornsilk', + 'crimson', + 'cyan', + 'darkblue', + 'darkcyan', + 'darkgoldenrod', + 'darkgray', + 'darkgreen', + 'darkgrey', + 'darkkhaki', + 'darkmagenta', + 'darkolivegreen', + 'darkorange', + 'darkorchid', + 'darkred', + 'darksalmon', + 'darkseagreen', + 'darkslateblue', + 'darkslategray', + 'darkslategrey', + 'darkturquoise', + 'darkviolet', + 'deeppink', + 'deepskyblue', + 'dimgray', + 'dimgrey', + 'dodgerblue', + 'firebrick', + 'floralwhite', + 'forestgreen', + 'fuchsia', + 'gainsboro', + 'ghostwhite', + 'gold', + 'goldenrod', + 'gray', + 'green', + 'greenyellow', + 'grey', + 'honeydew', + 'hotpink', + 'indianred', + 'indigo', + 'ivory', + 'khaki', + 'lavender', + 'lavenderblush', + 'lawngreen', + 'lemonchiffon', + 'lightblue', + 'lightcoral', + 'lightcyan', + 'lightgoldenrodyellow', + 'lightgray', + 'lightgreen', + 'lightgrey', + 'lightpink', + 'lightsalmon', + 'lightseagreen', + 'lightskyblue', + 'lightslategray', + 'lightslategrey', + 'lightsteelblue', + 'lightyellow', + 'lime', + 'limegreen', + 'linen', + 'magenta', + 'maroon', + 'mediumaquamarine', + 'mediumblue', + 'mediumorchid', + 'mediumpurple', + 'mediumseagreen', + 'mediumslateblue', + 'mediumspringgreen', + 'mediumturquoise', + 'mediumvioletred', + 'midnightblue', + 'mintcream', + 'mistyrose', + 'moccasin', + 'navajowhite', + 'navy', + 'oldlace', + 'olive', + 'olivedrab', + 'orange', + 'orangered', + 'orchid', + 'palegoldenrod', + 'palegreen', + 'paleturquoise', + 'palevioletred', + 'papayawhip', + 'peachpuff', + 'peru', + 'pink', + 'plum', + 'powderblue', + 'purple', + 'rebeccapurple', + 'red', + 'rosybrown', + 'royalblue', + 'saddlebrown', + 'salmon', + 'sandybrown', + 'seagreen', + 'seashell', + 'sienna', + 'silver', + 'skyblue', + 'slateblue', + 'slategray', + 'slategrey', + 'snow', + 'springgreen', + 'steelblue', + 'tan', + 'teal', + 'thistle', + 'tomato', + 'turquoise', + 'violet', + 'wheat', + 'white', + 'whitesmoke', + 'yellow', + 'yellowgreen', +] + +/** Hex, color functions and the colors module's `color.$*` namespace, anywhere in code. */ +const LITERAL = /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(|\bcolors?\.\$/ +/** A named color as a whole identifier (not part of `$brand-red`, `white-space` or `t.$white`). */ +const NAMED = new RegExp(`(? comment.replace(/[^\n]/g, ' ')).replace(/\/\/.*$/gm, '') +} + +/** + * Lines of `text` (from `file`) that hard-code a color or reach into the colors module. + * Lines carrying `// formeo-color-literal-ok: ` are exempt. + * @returns {string[]} `file:line: source` for each problem line + */ +export function findColorLiterals(text, file) { + const raw = text.split('\n') + const code = stripComments(text).split('\n') + const namespaces = code.map(line => line.match(COLORS_USE)).filter(Boolean) + const namespaceUse = namespaces + .map(match => match[2] ?? 'colors') + .filter(ns => !['color', 'colors', '*'].includes(ns)) + .map(ns => new RegExp(`(? { + if (raw[i].includes(OK)) return + const trimmed = line.trim() + const value = !trimmed.startsWith('@') && !trimmed.endsWith('{') ? line.match(DECLARATION)?.[1] : undefined + const flagged = + LITERAL.test(line) || + COLORS_USE.test(line) || + namespaceUse.some(re => re.test(line)) || + (value !== undefined && NAMED.test(value)) + if (flagged) problems.push(`${file}:${i + 1}: ${raw[i].trim()}`) + }) + return problems +} + +/** Check every guarded file; returns the problem lines. */ +export function checkGuardedFiles() { + return GUARDED_FILES.flatMap(file => + findColorLiterals(readFileSync(new URL(file, pathToFileURL(root)), 'utf8'), file) + ) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const problems = checkGuardedFiles() + if (problems.length) { + console.error(`Color literals / colors.$* found (use tokens.$*):\n${problems.join('\n')}`) + process.exit(1) + } +} diff --git a/tools/compile-scss.mjs b/tools/compile-scss.mjs new file mode 100644 index 00000000..55e3a24e --- /dev/null +++ b/tools/compile-scss.mjs @@ -0,0 +1,9 @@ +import { fileURLToPath } from 'node:url' +import * as sass from 'sass-embedded' + +const entry = fileURLToPath(new URL('../src/lib/sass/formeo.scss', import.meta.url)) + +/** Compile formeo.scss exactly as the lib build does (modern compiler, expanded output). */ +export function compileFormeoCss() { + return sass.compile(entry, { style: 'expanded', silenceDeprecations: ['import'] }).css +} diff --git a/tools/resolve-formeo-properties.mjs b/tools/resolve-formeo-properties.mjs new file mode 100644 index 00000000..57b383b9 --- /dev/null +++ b/tools/resolve-formeo-properties.mjs @@ -0,0 +1,71 @@ +const escapeSelector = selector => selector.replace(/[()[\].]/g, m => `\\${m}`) + +/** A whole rule whose selector is exactly `selectorSource` (a regex source), starting where a rule may start. */ +const rule = selectorSource => new RegExp(String.raw`(?<=(?:^|[{};]|\*/)\s*)${selectorSource}\s*\{([^}]*)\}\s*`, 'g') + +function splitDeclarations(body = '') { + return body + .split(';') + .map(decl => decl.trim()) + .filter(Boolean) +} + +function readDeclarations(body) { + const map = new Map() + for (const decl of splitDeclarations(body)) { + const match = decl.match(/^(--formeo-[\w-]+)\s*:\s*([\s\S]+)$/) + if (match) map.set(match[1], match[2]) + } + return map +} + +/** Remove the single `selector { … }` property block, throwing if it appears more than once or holds other rules. */ +function takeBlock(css, selector, allowed) { + const re = rule(escapeSelector(selector)) + const matches = [...css.matchAll(re)] + if (matches.length > 1) { + throw new Error(`Expected at most one ${selector} block, found ${matches.length}`) + } + const body = matches[0]?.[1] + const stray = splitDeclarations(body).filter(decl => !allowed.test(decl)) + if (stray.length) { + throw new Error(`${selector} may only declare ${allowed.source}; found: ${stray.join('; ')}`) + } + return { body, css: css.replace(re, '') } +} + +/** Rules intentionally added by this change; removed before comparing to baseline. */ +export const ALLOWED_ADDITIONS = [ + rule(String.raw`:where\(\.svg-icon\)`), + rule(String.raw`:where\(\.editing-field\)`), + rule( + String.raw`:where\(\.formeo-dark\) :where\(\.formeo, \.formeo-controls, \.formeo-dialog\),\s*` + + String.raw`:where\(\.formeo-dark\):where\(\.formeo, \.formeo-controls, \.formeo-dialog\)` + ), + rule( + String.raw`\.formeo-dark \.formeo\.formeo-editor \.conditions-prop-inputs label\.condition-label\.then-condition-label,\s*` + + String.raw`\.formeo-dark\.formeo\.formeo-editor \.conditions-prop-inputs label\.condition-label\.then-condition-label` + ), + rule( + String.raw`:where\(\.formeo-dark\) :where\(\.formeo-dialog\),\s*:where\(\.formeo-dark\):where\(\.formeo-dialog\)` + ), +] + +/** + * Replace every var(--formeo-*) with its :where(:root) default and drop the property blocks, + * producing CSS comparable to the pre-change baseline. + */ +export function resolveFormeoProperties(css) { + const root = takeBlock(css, ':where(:root)', /^--formeo-[\w-]+\s*:/) + const darkBlock = takeBlock(root.css, ':where(.formeo-dark)', /^(--formeo-[\w-]+|color-scheme)\s*:/) + const defaults = readDeclarations(root.body) + const dark = readDeclarations(darkBlock.body) + + let out = darkBlock.css + for (const re of ALLOWED_ADDITIONS) out = out.replace(re, '') + out = out.replace(/var\((--formeo-[\w-]+)\)/g, (_, name) => { + if (!defaults.has(name)) throw new Error(`No default declared for ${name}`) + return defaults.get(name) + }) + return { css: out, defaults, dark } +}