1
0
mirror of https://github.com/tabler/tabler.git synced 2026-08-05 19:03:18 +04:00

Update contributing guidelines to clarify custom property naming conventions and add postcss-prefix-custom-properties dependency for improved CSS processing.

This commit is contained in:
Bartek
2026-08-05 12:42:21 +02:00
parent db3f038cb9
commit a618d9b5bb
13 changed files with 214 additions and 10 deletions
+9 -1
View File
@@ -31,9 +31,11 @@ import { basename, join, resolve } from 'node:path'
import { compile as compileSass } from 'sass'
import postcss, { type Result } from 'postcss'
import autoprefixer from 'autoprefixer'
import prefixCustomProperties from 'postcss-prefix-custom-properties'
import rtlcss from 'rtlcss'
import CleanCSS from 'clean-css'
import { addBanner } from '../shared/banner/index.mjs'
import { cssVarIgnore, cssVarPrefix, inlineValueComments } from './css-var-prefix'
const args = process.argv.slice(2)
const flags = args.filter((arg) => arg.startsWith('--'))
@@ -47,6 +49,7 @@ const withMinify = flags.includes('--minify')
const withBanner = flags.includes('--banner')
const mapOptions = { inline: false, annotation: true, sourcesContent: true }
const written: string[] = []
const pendingWrites: { file: string; content: string }[] = []
@@ -68,7 +71,12 @@ async function compile(entry: string): Promise<{ outFile: string; result: Result
// Banner goes in before postcss runs, so the map it generates already counts
// the banner's lines — and so does the rtl map chained onto it below.
const input = withBanner ? addBanner(css, outFile) : css
const result = await postcss([autoprefixer({ cascade: false })]).process(input, {
// Prefixing is its own pass, ahead of the one that generates the map:
// postcss maps generated positions back to *its own input*, so renaming
// inside the map-generating pass would leave every mapping — and the
// embedded sourcesContent — describing css that no longer exists on disk.
const { css: prefixed } = await postcss([inlineValueComments, prefixCustomProperties({ prefix: cssVarPrefix, ignore: cssVarIgnore })]).process(input, { from: outFile, to: outFile, map: false })
const result = await postcss([autoprefixer({ cascade: false })]).process(prefixed, {
from: outFile,
to: outFile,
map: mapOptions,
+42
View File
@@ -0,0 +1,42 @@
// The `--tblr-` custom-property prefix, applied at build time.
//
// Custom properties are authored bare in scss (`--card-bg`) rather than through
// a Sass interpolation on every declaration and every var() call; the public
// prefix is added here, in the css pipeline (see build-css.ts).
//
// Lives in its own module so scss/tests/css-var-prefix.test.mjs asserts against
// the same configuration the build uses.
import type { Root } from 'postcss'
export const cssVarPrefix = 'tblr-'
// Names that must survive unprefixed. Everything here belongs to a third-party
// stylesheet we theme, and those libraries read their variables by their own
// name — prefixing one detaches the theming with no error anywhere, in css or
// at build time. Add an entry whenever a vendor override introduces a new
// foreign name.
export const cssVarIgnore = [
/^--tblr-/, // already prefixed, e.g. names that cannot round-trip postcss (see $navbar-light-icon-color)
/^--bs-/, // bootstrap
/^--fc-/, // fullcalendar
/^--gl-/, // star-rating.js
/^--litepicker-/, // litepicker
/^--plyr-/, // plyr
/^--ts-/, // tom-select
'--section-bg', // marketing sections, unprefixed since it was introduced
]
// postcss parks a comment trailing a declaration value in `raws.value.raw` and
// hands plugins the cleaned `value`, so any plugin that writes `decl.value`
// back drops the comment. That silently ate every `/*rtl:ignore*/` marker — the
// ones rtlcss reads to leave a declaration alone in the .rtl build. Folding raw
// into value first costs nothing (it stringifies identically) and leaves the
// comment in the value proper, where postcss-value-parser round-trips it.
export const inlineValueComments = {
postcssPlugin: 'tabler-inline-value-comments',
Once(root: Root) {
root.walkDecls((decl) => {
if (decl.raws.value) decl.value = decl.raws.value.raw
})
},
}
+5
View File
@@ -5,6 +5,11 @@ declare module 'rtlcss' {
export default function rtlcss(config?: unknown): Plugin
}
declare module 'postcss-prefix-custom-properties' {
import type { Plugin } from 'postcss'
export default function prefixCustomProperties(options?: { prefix?: string; ignore?: (string | RegExp)[] }): Plugin
}
declare module 'clean-css' {
interface MinifyResult {
styles: string
@@ -0,0 +1,5 @@
---
"@tabler/core": minor
---
Removed the `$prefix` Sass variable and its `--#{$prefix}` interpolations; the `--tblr-` custom property prefix is now applied at build time by `postcss-prefix-custom-properties`. Compiled CSS is unchanged, but the prefix can no longer be customised by overriding `$prefix` before importing the Sass sources.
+1 -1
View File
@@ -32,7 +32,7 @@ Do not edit `dist/` folders - they are generated by the build.
## Code style
- Use Bootstrap 5 conventions and Tabler's CSS custom properties pattern: `--#{$prefix}component-property`.
- Use Bootstrap 5 conventions and Tabler's CSS custom properties pattern: `--component-property`. Write custom properties without a prefix — the build adds the public `--tblr-` prefix. Names owned by a third-party library you are theming must be listed in `cssVarIgnore` in `.build/css-var-prefix.ts` so they stay untouched.
- Write documentation in simple English: short sentences, common words, direct instructions.
- Ensure changes work in all supported browsers (see the [browser support docs](https://docs.tabler.io/ui/getting-started/browser-support/)).
+4 -1
View File
@@ -1541,7 +1541,10 @@ $navbar-light-active-color: var(--body-color) !default;
$navbar-light-hover-color: var(--body-color) !default;
$navbar-light-disabled-color: var(--disabled-color) !default;
$navbar-light-active-bg: rgba(0, 0, 0, 0.2) !default;
$navbar-light-icon-color: color-transparent(var(--body-color), 0.75) !default;
// Spelled with the literal prefix: this value is url-encoded into the svg data
// uri below, so the build-time prefixing in .build/build-css.ts never sees a
// `var()` here to rewrite (it arrives as `var%28--body-color%29`).
$navbar-light-icon-color: color-transparent(var(--tblr-body-color), 0.75) !default;
$navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-icon-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>") !default;
$navbar-light-toggler-border-color: color-transparent(var(--emphasis-color), 0.15) !default;
$navbar-light-brand-hover-color: $navbar-light-active-color !default;
@@ -0,0 +1,64 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`css custom-property prefixing > leaves vendor-owned names alone and prefixes everything else 1`] = `
[
"--fc-border-color",
"--fc-daygrid-event-dot-width",
"--fc-event-bg-color",
"--fc-event-border-color",
"--fc-event-text-color",
"--gl-star-color",
"--gl-star-color-inactive",
"--gl-star-size",
"--litepicker-button-next-month-color",
"--litepicker-button-next-month-color-hover",
"--litepicker-button-prev-month-color",
"--litepicker-button-prev-month-color-hover",
"--litepicker-container-months-color-bg",
"--litepicker-day-color",
"--litepicker-day-color-hover",
"--litepicker-is-end-color-bg",
"--litepicker-is-in-range-color",
"--litepicker-is-start-color-bg",
"--litepicker-is-today-color",
"--litepicker-month-header-color",
"--litepicker-month-weekday-color",
"--plyr-color-main",
"--tblr-active-bg",
"--tblr-backdrop-bg",
"--tblr-bg-forms",
"--tblr-bg-surface",
"--tblr-bg-surface-inverted",
"--tblr-bg-surface-secondary",
"--tblr-body-bg",
"--tblr-body-color",
"--tblr-border-color",
"--tblr-border-color-translucent",
"--tblr-border-radius",
"--tblr-border-style",
"--tblr-border-width",
"--tblr-card-bg",
"--tblr-font-sans-serif",
"--tblr-font-size-h3",
"--tblr-font-size-h5",
"--tblr-font-weight-medium",
"--tblr-font-weight-semibold",
"--tblr-gray-500",
"--tblr-icon-size",
"--tblr-info",
"--tblr-light",
"--tblr-primary",
"--tblr-primary-200",
"--tblr-primary-lt",
"--tblr-secondary",
"--tblr-secondary-bg",
"--tblr-shadow-card",
"--tblr-shadow-dropdown",
"--tblr-shadow-input",
"--tblr-tertiary",
"--tblr-text-inverted",
"--tblr-yellow",
"--ts-pr-caret",
"--ts-pr-clear-button",
]
`;
+3 -3
View File
@@ -1,7 +1,7 @@
// Tests for the CSS custom-property builders in scss/mixins/_functions.scss:
// `varify()` and `rgba-css-var()`. Both read the global `$prefix`, which lives
// in scss/_settings.scss (already `@use`d by functions.scss), so a plain `@use`
// of the functions module is enough no fixtures.
// `varify()` and `rgba-css-var()`. A plain `@use` of the functions module is
// enough no fixtures. Both emit unprefixed names: the `--tblr-` prefix is
// added later, by the css pipeline (.build/css-var-prefix.ts).
@use 'sass:meta';
@use '../mixins/functions' as *;
+3 -2
View File
@@ -1,8 +1,9 @@
// Output tests for the `generate-utility` mixin from
// scss/mixins/bootstrap/_utilities.scss the engine behind Tabler's utility
// classes. `@use "../mixins/bootstrap/utilities"` pulls in the real
// settings/variables chain, so `$prefix` = "tblr-" and
// `$enable-important-utilities` = true (hence the `!important` in the output).
// settings/variables chain, so `$enable-important-utilities` = true (hence the
// `!important` in the output). Custom properties are expected unprefixed: the
// `--tblr-` prefix is added later, by the css pipeline (.build/css-var-prefix.ts).
//
// Each `it` feeds the mixin a hand-built utility map and asserts the generated
// classes, covering the branches that shape the output: map vs list values,
+3 -2
View File
@@ -2,8 +2,9 @@
//
// `@use "../mixins/mixins"` transitively pulls in the real settings/variables
// chain (all relative to that file, so no extra load paths are needed), which
// means the assertions run against real values `$prefix` = "tblr-", the real
// `$h5-font-size`, etc. instead of fixtures.
// means the assertions run against real values the real `$h5-font-size`,
// etc. instead of fixtures. Custom properties are expected unprefixed: the
// `--tblr-` prefix is added later, by the css pipeline (.build/css-var-prefix.ts).
@use '../mixins/mixins' as *;
+61
View File
@@ -0,0 +1,61 @@
// Guards the build-time `--tblr-` prefixing configured in .build/css-var-prefix.ts.
//
// Custom properties are authored bare in scss, so a name Tabler owns and a name
// a vendor library owns look identical in source — only `cssVarIgnore` tells
// them apart. Get that wrong and nothing fails: the vendor's variable quietly
// becomes `--tblr-fc-border-color`, the library never reads it, and the theming
// is simply gone.
//
// tabler-vendors.scss is where every foreign name lives, so its full set of
// custom-property names is snapshotted. A vendor override added without the
// matching `cssVarIgnore` entry shows up in the snapshot diff as a `--tblr-`
// name that plainly belongs to someone else.
import { describe, expect, it } from 'vitest'
import { readdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { compile as compileSass } from 'sass'
import postcss from 'postcss'
import prefixCustomProperties from 'postcss-prefix-custom-properties'
import { cssVarIgnore, cssVarPrefix, inlineValueComments } from '../../../.build/css-var-prefix.ts'
const scssDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
// The same two passes build-css.ts runs before autoprefixer.
async function buildCustomPropertyNames(entry) {
const { css } = compileSass(path.join(scssDir, entry), { loadPaths: ['node_modules'], style: 'expanded' })
const result = await postcss([
inlineValueComments,
prefixCustomProperties({ prefix: cssVarPrefix, ignore: cssVarIgnore }),
]).process(css, { from: undefined })
const names = new Set()
result.root.walkDecls((decl) => {
if (decl.prop.startsWith('--')) names.add(decl.prop)
for (const [, name] of decl.value.matchAll(/var\(\s*(--[\w-]+)/g)) names.add(name)
})
result.root.walkAtRules('property', (atRule) => names.add(atRule.params.trim()))
return [...names].sort()
}
describe('css custom-property prefixing', () => {
it('leaves vendor-owned names alone and prefixes everything else', async () => {
await expect(buildCustomPropertyNames('tabler-vendors.scss')).resolves.toMatchSnapshot()
})
it('has no dead entries in the ignore list', async () => {
// A pattern matching nothing means the vendor it protected is gone, or the
// name drifted — either way the entry no longer guards anything.
// Every entry point, since foreign names are spread across the bundles.
const entries = readdirSync(scssDir).filter((file) => file.endsWith('.scss') && !file.startsWith('_'))
const perEntry = await Promise.all(entries.map(buildCustomPropertyNames))
const all = [...new Set(perEntry.flat())]
for (const pattern of cssVarIgnore) {
const matches =
pattern instanceof RegExp ? all.some((name) => pattern.test(name)) : all.includes(pattern)
expect(matches, `unused cssVarIgnore entry: ${pattern}`).toBe(true)
}
})
})
+1
View File
@@ -47,6 +47,7 @@
"nodemon": "^3.1.14",
"pnpm": "10.34.4",
"postcss": "^8.5.23",
"postcss-prefix-custom-properties": "^0.1.0",
"prettier": "^3.9.6",
"prettier-plugin-astro": "^0.14.1",
"rtlcss": "^4.3.0",
+13
View File
@@ -60,6 +60,9 @@ importers:
postcss:
specifier: ^8.5.23
version: 8.5.23
postcss-prefix-custom-properties:
specifier: ^0.1.0
version: 0.1.0(postcss@8.5.23)
prettier:
specifier: ^3.9.6
version: 3.9.6
@@ -3673,6 +3676,11 @@ packages:
engines: {node: '>=18.12'}
hasBin: true
postcss-prefix-custom-properties@0.1.0:
resolution: {integrity: sha512-sAKQSRw+KTl9G549Q9awV/ra06fOZNEIVIIRTwiSTbvkV1t8z7WPqNQ5XBbxF1fV2mp0yQFObavc5J/GxP0a6g==}
peerDependencies:
postcss: ^8.4.0
postcss-scss@4.0.9:
resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==}
engines: {node: '>=12.0'}
@@ -8464,6 +8472,11 @@ snapshots:
pnpm@10.34.4: {}
postcss-prefix-custom-properties@0.1.0(postcss@8.5.23):
dependencies:
postcss: 8.5.23
postcss-value-parser: 4.2.0
postcss-scss@4.0.9(postcss@8.5.23):
dependencies:
postcss: 8.5.23