diff --git a/.agents/skills/backward-compat/SKILL.md b/.agents/skills/backward-compat/SKILL.md index a212c12c3..70859398f 100644 --- a/.agents/skills/backward-compat/SKILL.md +++ b/.agents/skills/backward-compat/SKILL.md @@ -1,7 +1,7 @@ --- name: backward-compat description: >- - Keep a change to `@tabler/core` backward compatible, so a patch or minor release never breaks a project that updates. Use before renaming, removing or changing the meaning of anything a user can touch — a class, a `--tblr-*` custom property, a Sass variable, mixin or function, a `data-*` attribute or option, a JS export, an event, a file under `dist/`, a default value — and whenever a changeset on `dev` looks like it needs a `major` bump. Also consult it proactively when a cleanup or refactor in `core/` deletes something, since "dead" code is often somebody's API. Covers what counts as public API, the deprecated-alias patterns, how to diff the public surface against the last release, and what to do when a break cannot be avoided. + Keep a change to `@tabler/core` backward compatible, so a patch or minor release never breaks a project that updates. Use before renaming, removing or changing the meaning of anything a user can touch — a class, a `--tblr-*` custom property, a Sass variable, mixin or function, a `data-*` attribute or option, a JS export, an event, a file under `dist/`, a default value — and whenever a changeset on `dev` looks like it needs a `major` bump. Also consult it proactively when a cleanup or refactor in `core/` deletes something, since "dead" code is often somebody's API. Covers what counts as public API, the `deprecated` files where compatibility code lives, the alias patterns, the `check:compat` gate, and what to do when a break cannot be avoided. --- # Backward compatibility @@ -32,16 +32,37 @@ Not public: anything under `preview/`, `docs/`, `shared/`, `.build/`, file layou Bug fixes are fine even when they change rendering, as long as the old behaviour was clearly wrong (a knob that did not move in RTL). When a fix changes how a correct page looks, treat it as a default change. -## 2. How to make the change compatible +## 2. Where compatibility code lives + +Everything kept only for old projects goes into files named `deprecated`, so that the next major removes it by deleting files instead of digging through the tree. The Sass module system decides how many files that is: + +| File | Holds | Why it is separate | +| --- | --- | --- | +| `core/scss/_variables-deprecated.scss` | old `$variables`, each `!default` | `@use … with ($x: …)` on a variable that does not exist is a compile error, so the variable has to stay declared. Forwarded from `_config.scss` **before** `variables`, so a new variable can default to the old one: `$focus-ring-width: $input-btn-focus-width !default;` | +| `core/scss/mixins/_deprecated.scss` | old functions and mixins as thin wrappers around the new ones | emits no CSS; forwarded from `_mixins.scss`. Each one calls `@include deprecate('', '', '')` from `mixins/bootstrap/_deprecate.scss`, so the user gets a warning when compiling | +| `core/scss/_deprecated.scss` | everything that emits CSS: class aliases (`.form-hint`, `.legend:empty`), old custom properties such as `--tblr-*-rgb`, hooks like `box-shadow: var(--btn-focus-box-shadow, none)` | loaded last from `tabler.scss`, next to `_extends.scss`: an alias built with `@extend` needs the whole `core` upstream. The whole file sits inside `@if $enable-deprecated { … }` | +| `core/js/src/deprecated.ts` | old exports and globals, such as the `tabler` namespace with `getColor()` | one import to delete in the next major | + +Create a file the first time you need it. `$enable-deprecated: true !default` lives in `_variables.scss`, next to `$enable-deprecation-messages`. A project that has migrated sets it to `false` and gets the bytes back; the next major flips the default, and the one after deletes the files. + +Some compatibility cannot move into those files: a fallback inside a component rule (`mask-image: var(--form-check-bg-image, )`), an old parameter in a mixin signature, a default value that was put back. Mark each of those where it is, with one fixed comment: + +```scss +// deprecated(2.0): `$show-border` is the old name of `$offset` +``` + +`grep -rn "deprecated(2.0)" core/` plus the four files is then the full list of what the next major removes. + +## 3. How to make the change compatible ### Rename Ship both names. The old one becomes an alias that nobody has to touch. -- **Class**: add the alias to `core/scss/_extends.scss` with `@extend`, next to `.form-hint`. Start the comment with `` `` is deprecated, use `` instead ``. +- **Class**: add the alias to `core/scss/_deprecated.scss` with `@extend`. Start the comment with `` `` is deprecated, use `` instead ``. - **Custom property**: keep reading the old name as a fallback — `var(--new, var(--old, ))` — or keep emitting the old one with the same value. -- **Sass variable**: keep the old variable and default the new one to it: `$new: $old !default;`. For a map key, accept both keys. -- **Mixin or function parameter**: add new parameters at the end, with defaults. Never rename a keyword argument; add a second one and map the old onto it. +- **Sass variable**: keep the old variable in `_variables-deprecated.scss` and default the new one to it: `$new: $old !default;`. For a map key, accept both keys. +- **Mixin or function**: a removed one comes back in `mixins/_deprecated.scss` as a wrapper. For parameters, add new ones at the end, with defaults. Never rename or move an old parameter: callers pass arguments by position and by keyword, so it keeps both its name and its place. - **Data attribute, option, JS export, event**: accept or fire both. The `data-bs-*` / `data-tblr-*` pair in `core/js/src/bootstrap/dom/manipulator.ts` is the pattern. ### Remove @@ -50,7 +71,7 @@ Do not. Mark it deprecated, stop using it in Tabler's own code, keep it working, ### Change what a name means -Do not reuse the name. `.legend` was a dot in 1.5 and became a whole legend item in 1.6, so old markup rendered as nothing. Either give the new thing a new name, or keep the old use working — `.legend:empty` in `_extends.scss` still draws the dot. +Do not reuse the name. `.legend` was a dot in 1.5 and became a whole legend item in 1.6, so old markup rendered as nothing. Either give the new thing a new name, or keep the old use working — `.legend:empty` still draws the dot. ### Add a name @@ -68,37 +89,41 @@ Keep the file in `dist/` and the entry in `core/libs.json` until the next major, New validation must not throw on input that used to work. Coerce the value, or log with `console.warn` and fall back to the default. A throw during page load stops all of `tabler.js`. -## 3. Check the public surface before the PR +## 4. Check the public surface before the PR -Build core, then diff the compiled CSS against the last release. Anything in the output is a removed class or custom property and needs an alias or a revert. +`check:compat` downloads the last release of `@tabler/core` from npm and compares it with the working tree. It needs `core/dist`, so build core first: ```shell -last=$(npm view @tabler/core version) -curl -s "https://cdn.jsdelivr.net/npm/@tabler/core@$last/dist/css/tabler.css" -o /tmp/released.css - -surface() { grep -oE "$1" "$2" | sort -u; } -class='\.[a-zA-Z][a-zA-Z0-9_-]*' -prop='--tblr-[a-zA-Z0-9_-]+' - -# classes, then custom properties, that the release has and the build lost -comm -23 <(surface "$class" /tmp/released.css) <(surface "$class" core/dist/css/tabler.css) -comm -23 <(surface -e"$prop" /tmp/released.css) <(surface -e"$prop" core/dist/css/tabler.css) +pnpm --filter @tabler/core build +pnpm run check:compat ``` -For the rest, read the diff against the release tag with the table from section 1 in hand: +It reports what the release has and the build lost, one key per line: + +| Key | Means | +| --- | --- | +| `css:tabler.css:.legend` | a class is gone from that stylesheet | +| `css:tabler.css:--tblr-primary-rgb` | a custom property is gone | +| `sass:$focus-ring-blur` | a Sass variable is gone | +| `sass:@function url-svg` | a function or mixin is gone | +| `sass:@mixin focus-ring($show-border)` | that parameter was renamed or moved | +| `js:tabler.esm.js:tabler` | a top-level export is gone | +| `file:dist/libs/litepicker/dist/litepicker.js` | a file is gone from the package | + +Fix each one with the patterns from section 3, then run it again. Known breaks are listed in `.build/compat-baseline.txt`; when you fix one, delete its line, or the check fails on the stale entry. Never add a line to the baseline to make a PR green. A line gets ` # accepted: ` only after the maintainers decided to ship that break. CI runs the check after the build, and with `--strict` on the versions pull request, so a release cannot be merged while an unaccepted line is left. + +The check cannot see everything. It does not know about data attributes and their options, events, default values, a Sass variable whose type changed, or behaviour. For those, read the diff against the release tag with the table from section 1 in hand: ```shell -git diff "@tabler/core@$last"..HEAD -- core/scss/_variables.scss core/scss/mixins core/js core/libs.json core/package.json .browserslistrc +git diff "@tabler/core@$(npm view @tabler/core version)"..HEAD -- core/scss/_variables.scss core/js .browserslistrc ``` -Look for deleted `$variables`, changed mixin signatures, removed exports in `core/js/tabler.ts`, removed `libs.json` entries and new or removed dependencies. +## 5. Changesets and the upgrade guide -## 4. Changesets and the upgrade guide - -- A changeset on `dev` is `patch` or `minor`. If the honest bump is `major`, the change is on the wrong branch — go back to section 2. +- A changeset on `dev` is `patch` or `minor`. If the honest bump is `major`, the change is on the wrong branch — go back to section 3. - Describe a deprecation as one, for example ``Added `.legend-dot`; an empty `.legend` still works but is deprecated.`` Never write `Removed` for something a user could have used. - In the upgrade guide a deprecation goes under a "renamed" heading with "the old name still works" (see the `upgrade-guide` skill). A minor release should have nothing under a breaking-change heading. -## 5. When a break cannot be avoided +## 6. When a break cannot be avoided Some changes have no alias: dropping a whole color model, raising the browser floor, removing a dependency users import. Those belong to the next major release, not to `dev`. Say so in the PR, and do not split the change to sneak half of it into a minor. If you are unsure whether something is a break, assume it is and ask. diff --git a/.agents/skills/core-scss/SKILL.md b/.agents/skills/core-scss/SKILL.md index e2f8b5eb2..633631503 100644 --- a/.agents/skills/core-scss/SKILL.md +++ b/.agents/skills/core-scss/SKILL.md @@ -121,4 +121,5 @@ pnpm run bundlewatch # size budgets (tabler.css 80 kB, tab - [ ] No class, custom property, Sass variable or mixin parameter removed or renamed without an alias (`backward-compat`) - [ ] sass-true test for a mixin that can break silently - [ ] `lint:scss`, `check:tokens`, `lint:prettier`, `test:scss` clean +- [ ] `check:compat` clean after a core build, with no new line in `.build/compat-baseline.txt` - [ ] Docs page and class table updated (`write-docs`, `class-reference`), changeset written diff --git a/.build/check-compat.ts b/.build/check-compat.ts new file mode 100644 index 000000000..0b2c9bc7d --- /dev/null +++ b/.build/check-compat.ts @@ -0,0 +1,246 @@ +// Fails when the build drops something the last release shipped. +// +// The README promises semver: a project on `^1.0.0` updates without touching its +// own code. A removed class, custom property, Sass variable or file breaks that +// promise, and nothing else notices — the build is green, the tests pass, and +// the first report comes from a user (#3108). This check downloads the last +// release of `@tabler/core` from npm and compares its public surface with the +// working tree: +// +// css classes and `--tblr-*` custom properties of every dist/css/*.css +// sass `$variables`, and `@function` / `@mixin` names and parameters, in scss/ +// js top-level exports of dist/js/tabler.esm.js +// file every path under dist/ (libs, images, types), and scss/ partials +// +// Known breaks are listed in compat-baseline.txt. A line ending in +// `# accepted: ` is a decision; a line without it is a break still to fix. +// The check fails on a break that is not listed, and on a listed one that no +// longer breaks, so fixing one means deleting its line. `--strict` also fails +// while any unaccepted line is left: the release runs it that way. +// +// Needs `core/dist`, so run it after the core build. See the `backward-compat` +// skill for how to fix what it reports. +// +// Usage: tsx .build/check-compat.ts [--strict] +import { execFileSync } from 'node:child_process' +import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { join, relative } from 'node:path' + +const pkgName = '@tabler/core' +const coreDir = 'core' +const baselineFile = '.build/compat-baseline.txt' +const cacheDir = 'node_modules/.cache/check-compat' +const strict = process.argv.includes('--strict') + +const registry = 'https://registry.npmjs.org' + +async function releasedDir(): Promise<{ dir: string; version: string }> { + const local = JSON.parse(readFileSync(join(coreDir, 'package.json'), 'utf8')).version as string + // On the versions PR the local version is not published yet: compare with `latest`. + let version = local + let response = await fetch(`${registry}/${pkgName}/${version}`) + if (response.status === 404) { + response = await fetch(`${registry}/${pkgName}/latest`) + } + if (!response.ok) throw new Error(`npm registry answered ${response.status} for ${pkgName}`) + const manifest = (await response.json()) as { version: string; dist: { tarball: string } } + version = manifest.version + + const dir = join(cacheDir, version) + if (!existsSync(join(dir, 'package', 'package.json'))) { + mkdirSync(dir, { recursive: true }) + const tarball = await fetch(manifest.dist.tarball) + if (!tarball.ok) throw new Error(`could not download ${manifest.dist.tarball}: ${tarball.status}`) + const archive = join(dir, 'package.tgz') + writeFileSync(archive, Buffer.from(await tarball.arrayBuffer())) + execFileSync('tar', ['-xzf', archive, '-C', dir]) + } + return { dir: join(dir, 'package'), version } +} + +function walk(root: string, dir = root): string[] { + if (!existsSync(dir)) return [] + return readdirSync(dir).flatMap((name) => { + const path = join(dir, name) + return statSync(path).isDirectory() ? walk(root, path) : [relative(root, path)] + }) +} + +const matches = (source: string, pattern: RegExp): Set => new Set([...source.matchAll(pattern)].map((match) => match[1]!)) + +// --- css ------------------------------------------------------------------- + +// A class is a dot followed by an identifier; `1.5rem` and `url(a.svg)` are not. +const cssClass = /(? css.replace(/\/\*[\s\S]*?\*\//g, '').replace(/url\([^)]*\)/g, 'url()') + +function cssBreaks(released: string, current: string): string[] { + const breaks: string[] = [] + const sheets = walk(join(released, 'dist/css')).filter((file) => file.endsWith('.css') && !file.includes('.min.') && !file.includes('.rtl.')) + + for (const sheet of sheets) { + const currentPath = join(current, 'dist/css', sheet) + if (!existsSync(currentPath)) continue // reported by fileBreaks + const before = stripCssNoise(readFileSync(join(released, 'dist/css', sheet), 'utf8')) + const after = stripCssNoise(readFileSync(currentPath, 'utf8')) + + const classesAfter = matches(after, cssClass) + for (const name of matches(before, cssClass)) { + if (!classesAfter.has(name)) breaks.push(`css:${sheet}:.${name}`) + } + const propertiesAfter = matches(after, cssProperty) + for (const name of matches(before, cssProperty)) { + if (!propertiesAfter.has(name)) breaks.push(`css:${sheet}:${name}`) + } + } + return breaks +} + +// --- sass ------------------------------------------------------------------ + +type Callable = { kind: string; name: string; params: string[] } + +const sassVariable = /^\$([\w-]+)\s*:/gm +const sassCallable = /^@(function|mixin)\s+([\w-]+)\s*(?:\(([\s\S]*?)\))?\s*\{/gm + +function splitParams(source: string | undefined): string[] { + if (!source) return [] + const params: string[] = [] + let depth = 0 + let current = '' + for (const char of source) { + if (char === '(') depth++ + if (char === ')') depth-- + if (char === ',' && depth === 0) { + params.push(current) + current = '' + } else { + current += char + } + } + params.push(current) + // `$color: rgba($white, 0.15)` → `$color`; `$args...` → `$args...` + return params.map((param) => param.trim().split(':')[0]!.trim()).filter(Boolean) +} + +function sassSurface(root: string): { variables: Set; callables: Map } { + const variables = new Set() + const callables = new Map() + for (const file of walk(join(root, 'scss'))) { + if (!file.endsWith('.scss') || file.startsWith('tests/')) continue + const source = readFileSync(join(root, 'scss', file), 'utf8').replace(/\/\/.*$/gm, '') + for (const name of matches(source, sassVariable)) variables.add(name) + for (const [, kind, name, params] of source.matchAll(sassCallable)) { + if (name!.startsWith('_') || name!.startsWith('-')) continue // private by convention + callables.set(`${kind} ${name}`, { kind: kind!, name: name!, params: splitParams(params) }) + } + } + return { variables, callables } +} + +function sassBreaks(released: string, current: string): string[] { + const breaks: string[] = [] + const before = sassSurface(released) + const after = sassSurface(current) + + for (const name of before.variables) { + if (!after.variables.has(name)) breaks.push(`sass:$${name}`) + } + for (const [key, callable] of before.callables) { + const now = after.callables.get(key) + if (!now) { + breaks.push(`sass:@${callable.kind} ${callable.name}`) + continue + } + // A caller passes arguments by position or by keyword: an old parameter has + // to keep both its name and its place. New ones go after it. + callable.params.forEach((param, index) => { + if (now.params[index] !== param) breaks.push(`sass:@${callable.kind} ${callable.name}(${param})`) + }) + } + return breaks +} + +// --- js -------------------------------------------------------------------- + +function esmExports(file: string): Set { + if (!existsSync(file)) return new Set() + const source = readFileSync(file, 'utf8') + const names = new Set() + for (const [, list] of source.matchAll(/^export\s*\{([^}]*)\}/gm)) { + for (const entry of list!.split(',')) { + const name = entry + .trim() + .split(/\s+as\s+/) + .pop() + if (name) names.add(name) + } + } + for (const name of matches(source, /^export\s+(?:const|let|var|function|class)\s+([\w$]+)/gm)) names.add(name) + if (/^export\s+default\b/m.test(source)) names.add('default') + return names +} + +function jsBreaks(released: string, current: string): string[] { + const breaks: string[] = [] + for (const bundle of ['tabler.esm.js', 'tabler-theme.esm.js']) { + const after = esmExports(join(current, 'dist/js', bundle)) + for (const name of esmExports(join(released, 'dist/js', bundle))) { + if (!after.has(name)) breaks.push(`js:${bundle}:${name}`) + } + } + return breaks +} + +// --- files ----------------------------------------------------------------- + +function fileBreaks(released: string, current: string): string[] { + const now = new Set([...walk(join(current, 'dist')).map((file) => `dist/${file}`), ...walk(join(current, 'scss')).map((file) => `scss/${file}`)]) + const before = [...walk(join(released, 'dist')).map((file) => `dist/${file}`), ...walk(join(released, 'scss')).map((file) => `scss/${file}`)] + return before.filter((file) => !file.endsWith('.map') && !file.startsWith('scss/tests/') && !now.has(file)).map((file) => `file:${file}`) +} + +// --- main ------------------------------------------------------------------ + +async function main() { + if (!existsSync(join(coreDir, 'dist/css/tabler.css'))) { + throw new Error('core/dist is missing — build core first: pnpm --filter @tabler/core build') + } + + const { dir: released, version } = await releasedDir() + const breaks = new Set([...cssBreaks(released, coreDir), ...sassBreaks(released, coreDir), ...jsBreaks(released, coreDir), ...fileBreaks(released, coreDir)]) + + const baseline = new Map() // key → accepted + for (const line of readFileSync(baselineFile, 'utf8').split('\n')) { + const [entry, comment = ''] = line.split(' # ') + const key = entry!.trim() + if (!key || key.startsWith('#')) continue + baseline.set(key, comment.trim().startsWith('accepted:')) + } + + const added = [...breaks].filter((key) => !baseline.has(key)).sort() + const fixed = [...baseline.keys()].filter((key) => !breaks.has(key)) + const todo = [...baseline].filter(([key, accepted]) => !accepted && breaks.has(key)).map(([key]) => key) + + for (const key of added) console.error(`✗ ${key}`) + if (added.length > 0) { + console.error(`\n${added.length} new ${added.length === 1 ? 'break' : 'breaks'} against ${pkgName}@${version}. Keep the old name working as a deprecated alias — see the backward-compat skill.`) + } + if (fixed.length > 0) { + console.error(`\nNo longer ${fixed.length === 1 ? 'a break' : 'breaks'} — delete from ${baselineFile}:\n${fixed.map((key) => ` ${key}`).join('\n')}`) + } + if (strict && todo.length > 0) { + console.error(`\n--strict: ${todo.length} listed ${todo.length === 1 ? 'break is' : 'breaks are'} neither fixed nor accepted:\n${todo.map((key) => ` ${key}`).join('\n')}`) + } + if (added.length > 0 || fixed.length > 0 || (strict && todo.length > 0)) process.exit(1) + + const accepted = baseline.size - todo.length + console.log(`OK — nothing new is missing compared with ${pkgName}@${version} (${todo.length} known breaks still to fix, ${accepted} accepted, in ${baselineFile}).`) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/.build/compat-baseline.txt b/.build/compat-baseline.txt new file mode 100644 index 000000000..8e7fbf174 --- /dev/null +++ b/.build/compat-baseline.txt @@ -0,0 +1,271 @@ +# What the working tree no longer ships compared with the last release of +# @tabler/core - see check-compat.ts. The check fails on any break that is not +# listed here, and on a listed one that no longer breaks, so fixing one means +# deleting its line. +# +# A plain line is a break that still has to be fixed before the release: +# `check-compat --strict` fails while one is left. Add ` # accepted: ` +# to a line only after the maintainers decided to ship the break, for example +# an override for a class that a third-party library dropped. +# +# Everything below entered `dev` between 1.5.1 and 1.6.0, before the check existed. + +# RGB triplets of the colors, removed with the move to color-mix() (#3042) +css:tabler-props.css:--tblr-azure-lt-rgb +css:tabler-props.css:--tblr-azure-rgb +css:tabler-props.css:--tblr-bitbucket-lt-rgb +css:tabler-props.css:--tblr-bitbucket-rgb +css:tabler-props.css:--tblr-blue-lt-rgb +css:tabler-props.css:--tblr-blue-rgb +css:tabler-props.css:--tblr-cyan-lt-rgb +css:tabler-props.css:--tblr-cyan-rgb +css:tabler-props.css:--tblr-danger-lt-rgb +css:tabler-props.css:--tblr-danger-rgb +css:tabler-props.css:--tblr-dark-lt-rgb +css:tabler-props.css:--tblr-dark-rgb +css:tabler-props.css:--tblr-dribbble-lt-rgb +css:tabler-props.css:--tblr-dribbble-rgb +css:tabler-props.css:--tblr-facebook-lt-rgb +css:tabler-props.css:--tblr-facebook-rgb +css:tabler-props.css:--tblr-flickr-lt-rgb +css:tabler-props.css:--tblr-flickr-rgb +css:tabler-props.css:--tblr-github-lt-rgb +css:tabler-props.css:--tblr-github-rgb +css:tabler-props.css:--tblr-google-lt-rgb +css:tabler-props.css:--tblr-google-rgb +css:tabler-props.css:--tblr-green-lt-rgb +css:tabler-props.css:--tblr-green-rgb +css:tabler-props.css:--tblr-indigo-lt-rgb +css:tabler-props.css:--tblr-indigo-rgb +css:tabler-props.css:--tblr-info-lt-rgb +css:tabler-props.css:--tblr-info-rgb +css:tabler-props.css:--tblr-instagram-lt-rgb +css:tabler-props.css:--tblr-instagram-rgb +css:tabler-props.css:--tblr-light-lt-rgb +css:tabler-props.css:--tblr-light-rgb +css:tabler-props.css:--tblr-lime-lt-rgb +css:tabler-props.css:--tblr-lime-rgb +css:tabler-props.css:--tblr-linkedin-lt-rgb +css:tabler-props.css:--tblr-linkedin-rgb +css:tabler-props.css:--tblr-muted-lt-rgb +css:tabler-props.css:--tblr-muted-rgb +css:tabler-props.css:--tblr-orange-lt-rgb +css:tabler-props.css:--tblr-orange-rgb +css:tabler-props.css:--tblr-pink-lt-rgb +css:tabler-props.css:--tblr-pink-rgb +css:tabler-props.css:--tblr-pinterest-lt-rgb +css:tabler-props.css:--tblr-pinterest-rgb +css:tabler-props.css:--tblr-primary-lt-rgb +css:tabler-props.css:--tblr-primary-rgb +css:tabler-props.css:--tblr-purple-lt-rgb +css:tabler-props.css:--tblr-purple-rgb +css:tabler-props.css:--tblr-red-lt-rgb +css:tabler-props.css:--tblr-red-rgb +css:tabler-props.css:--tblr-rss-lt-rgb +css:tabler-props.css:--tblr-rss-rgb +css:tabler-props.css:--tblr-secondary-lt-rgb +css:tabler-props.css:--tblr-secondary-rgb +css:tabler-props.css:--tblr-success-lt-rgb +css:tabler-props.css:--tblr-success-rgb +css:tabler-props.css:--tblr-tabler-lt-rgb +css:tabler-props.css:--tblr-tabler-rgb +css:tabler-props.css:--tblr-teal-lt-rgb +css:tabler-props.css:--tblr-teal-rgb +css:tabler-props.css:--tblr-twitter-lt-rgb +css:tabler-props.css:--tblr-twitter-rgb +css:tabler-props.css:--tblr-vimeo-lt-rgb +css:tabler-props.css:--tblr-vimeo-rgb +css:tabler-props.css:--tblr-vk-lt-rgb +css:tabler-props.css:--tblr-vk-rgb +css:tabler-props.css:--tblr-warning-lt-rgb +css:tabler-props.css:--tblr-warning-rgb +css:tabler-props.css:--tblr-x-lt-rgb +css:tabler-props.css:--tblr-x-rgb +css:tabler-props.css:--tblr-yellow-lt-rgb +css:tabler-props.css:--tblr-yellow-rgb +css:tabler-props.css:--tblr-youtube-lt-rgb +css:tabler-props.css:--tblr-youtube-rgb +css:tabler-themes.css:--tblr-primary-rgb +css:tabler.css:--tblr-azure-lt-rgb +css:tabler.css:--tblr-azure-rgb +css:tabler.css:--tblr-bitbucket-lt-rgb +css:tabler.css:--tblr-bitbucket-rgb +css:tabler.css:--tblr-black-rgb +css:tabler.css:--tblr-blue-lt-rgb +css:tabler.css:--tblr-blue-rgb +css:tabler.css:--tblr-body-bg-rgb +css:tabler.css:--tblr-body-color-rgb +css:tabler.css:--tblr-cyan-lt-rgb +css:tabler.css:--tblr-cyan-rgb +css:tabler.css:--tblr-danger-lt-rgb +css:tabler.css:--tblr-danger-rgb +css:tabler.css:--tblr-dark-lt-rgb +css:tabler.css:--tblr-dark-rgb +css:tabler.css:--tblr-dribbble-lt-rgb +css:tabler.css:--tblr-dribbble-rgb +css:tabler.css:--tblr-emphasis-color-rgb +css:tabler.css:--tblr-facebook-lt-rgb +css:tabler.css:--tblr-facebook-rgb +css:tabler.css:--tblr-flickr-lt-rgb +css:tabler.css:--tblr-flickr-rgb +css:tabler.css:--tblr-github-lt-rgb +css:tabler.css:--tblr-github-rgb +css:tabler.css:--tblr-google-lt-rgb +css:tabler.css:--tblr-google-rgb +css:tabler.css:--tblr-green-lt-rgb +css:tabler.css:--tblr-green-rgb +css:tabler.css:--tblr-indigo-lt-rgb +css:tabler.css:--tblr-indigo-rgb +css:tabler.css:--tblr-info-lt-rgb +css:tabler.css:--tblr-info-rgb +css:tabler.css:--tblr-instagram-lt-rgb +css:tabler.css:--tblr-instagram-rgb +css:tabler.css:--tblr-light-lt-rgb +css:tabler.css:--tblr-light-rgb +css:tabler.css:--tblr-lime-lt-rgb +css:tabler.css:--tblr-lime-rgb +css:tabler.css:--tblr-link-color-rgb +css:tabler.css:--tblr-link-hover-color-rgb +css:tabler.css:--tblr-linkedin-lt-rgb +css:tabler.css:--tblr-linkedin-rgb +css:tabler.css:--tblr-muted-lt-rgb +css:tabler.css:--tblr-muted-rgb +css:tabler.css:--tblr-orange-lt-rgb +css:tabler.css:--tblr-orange-rgb +css:tabler.css:--tblr-pink-lt-rgb +css:tabler.css:--tblr-pink-rgb +css:tabler.css:--tblr-pinterest-lt-rgb +css:tabler.css:--tblr-pinterest-rgb +css:tabler.css:--tblr-primary-lt-rgb +css:tabler.css:--tblr-primary-rgb +css:tabler.css:--tblr-purple-lt-rgb +css:tabler.css:--tblr-purple-rgb +css:tabler.css:--tblr-red-lt-rgb +css:tabler.css:--tblr-red-rgb +css:tabler.css:--tblr-rss-lt-rgb +css:tabler.css:--tblr-rss-rgb +css:tabler.css:--tblr-secondary-bg-rgb +css:tabler.css:--tblr-secondary-color-rgb +css:tabler.css:--tblr-secondary-lt-rgb +css:tabler.css:--tblr-secondary-rgb +css:tabler.css:--tblr-status-color-rgb +css:tabler.css:--tblr-success-lt-rgb +css:tabler.css:--tblr-success-rgb +css:tabler.css:--tblr-tabler-lt-rgb +css:tabler.css:--tblr-tabler-rgb +css:tabler.css:--tblr-teal-lt-rgb +css:tabler.css:--tblr-teal-rgb +css:tabler.css:--tblr-tertiary-bg-rgb +css:tabler.css:--tblr-tertiary-color-rgb +css:tabler.css:--tblr-twitter-lt-rgb +css:tabler.css:--tblr-twitter-rgb +css:tabler.css:--tblr-vimeo-lt-rgb +css:tabler.css:--tblr-vimeo-rgb +css:tabler.css:--tblr-vk-lt-rgb +css:tabler.css:--tblr-vk-rgb +css:tabler.css:--tblr-warning-lt-rgb +css:tabler.css:--tblr-warning-rgb +css:tabler.css:--tblr-white-rgb +css:tabler.css:--tblr-x-lt-rgb +css:tabler.css:--tblr-x-rgb +css:tabler.css:--tblr-yellow-lt-rgb +css:tabler.css:--tblr-yellow-rgb +css:tabler.css:--tblr-youtube-lt-rgb +css:tabler.css:--tblr-youtube-rgb + +# Other custom properties: focus box-shadow tokens (#3098), form hint (#3059), masked icons (#3043) +css:tabler-props.css:--tblr-bg-surface-dark +css:tabler.css:--tblr-accordion-btn-focus-box-shadow +css:tabler.css:--tblr-btn-close-focus-shadow +css:tabler.css:--tblr-btn-focus-box-shadow +css:tabler.css:--tblr-focus-ring-blur +css:tabler.css:--tblr-focus-ring-x +css:tabler.css:--tblr-focus-ring-y +css:tabler.css:--tblr-form-check-bg-image +css:tabler.css:--tblr-form-hint-control-margin-top +css:tabler.css:--tblr-form-hint-label-offset +css:tabler.css:--tblr-form-hint-margin-top +css:tabler.css:--tblr-form-switch-bg +css:tabler.css:--tblr-pagination-focus-box-shadow + +# tabler-vendors.css: Litepicker styles (#3062) and overrides for classes of third-party libraries +css:tabler-vendors.css:--tblr-bg-forms +css:tabler-vendors.css:--tblr-info +css:tabler-vendors.css:--tblr-light +css:tabler-vendors.css:.apexcharts-yaxis-tick +css:tabler-vendors.css:.button-next-month +css:tabler-vendors.css:.button-previous-month +css:tabler-vendors.css:.container__days +css:tabler-vendors.css:.container__main +css:tabler-vendors.css:.container__months +css:tabler-vendors.css:.datepicker-inline +css:tabler-vendors.css:.day-item +css:tabler-vendors.css:.fslightbox-slash +css:tabler-vendors.css:.fslightbox-slide-number-container +css:tabler-vendors.css:.jvm-legend-inner +css:tabler-vendors.css:.litepicker +css:tabler-vendors.css:.month-item-name +css:tabler-vendors.css:.month-item-weekdays-row +css:tabler-vendors.css:.month-item-year +css:tabler-vendors.css:.ts-input + +# Sass variables +sass:$accordion-btn-focus-box-shadow +sass:$btn-close-focus-shadow +sass:$btn-focus-box-shadow +sass:$focus-ring-blur +sass:$focus-ring-box-shadow +sass:$form-check-input-checked-bg-image-dark +sass:$form-check-input-focus-box-shadow +sass:$form-hint-control-margin-top +sass:$form-hint-label-offset +sass:$form-hint-margin-top +sass:$form-range-thumb-focus-box-shadow +sass:$form-select-focus-box-shadow +sass:$form-select-focus-width +sass:$form-switch-bg-image +sass:$form-switch-bg-image-dark +sass:$form-switch-checked-bg-image +sass:$form-switch-checked-bg-position +sass:$form-switch-color-dark +sass:$form-switch-focus-bg-image +sass:$input-btn-focus-blur +sass:$input-btn-focus-box-shadow +sass:$input-btn-focus-color +sass:$input-btn-focus-color-opacity +sass:$input-btn-focus-width +sass:$input-focus-box-shadow +sass:$input-focus-width +sass:$nav-link-focus-box-shadow +sass:$navbar-dark-toggler-icon-bg +sass:$navbar-light-toggler-icon-bg +sass:$pagination-focus-box-shadow +sass:$pagination-focus-outline +sass:$theme-colors-rgb + +# Sass functions and mixins; `name($param)` means that parameter moved or was renamed +sass:@function breakpoint-prev +sass:@function map-get-multiple +sass:@function map-merge-multiple +sass:@function theme-color-darker +sass:@function theme-color-lighter +sass:@function url-svg +sass:@function varify +sass:@mixin focus-ring($show-border) +sass:@mixin form-validation-state($focus-box-shadow) +sass:@mixin media-breakpoint-down-than + +# JavaScript exports: the `tabler` namespace with getColor(), hexToRgba() and prefix +js:tabler.esm.js:tabler + +# Files in the package +file:dist/libs/autosize/LICENSE.md +file:dist/libs/autosize/dist/autosize.min.js +file:dist/libs/clipboard/LICENSE +file:dist/libs/clipboard/dist/clipboard.min.js +file:dist/libs/countup.js/LICENSE.md +file:dist/libs/countup.js/dist/countUp.umd.js +file:dist/libs/litepicker/LICENSE.md +file:dist/libs/litepicker/dist/litepicker.js +file:dist/types/src/tabler.d.ts +file:scss/vendor/_litepicker.scss diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7829e11ec..5f3d19598 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,3 +22,11 @@ jobs: # turbo directly: `pnpm run build` also zips the package, which only the release needs - name: Build run: pnpm exec turbo build + + # Nothing the last release shipped may disappear in a minor release. The + # versions pull request runs it with --strict, so a release cannot be + # merged while compat-baseline.txt still lists a break nobody accepted. + - name: Check backward compatibility + env: + STRICT: ${{ github.head_ref == 'changeset-release/dev' && '--strict' || '' }} + run: pnpm run check:compat $STRICT diff --git a/package.json b/package.json index c1ec17829..549835825 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "check:sri": "tsx .build/generate-sri.ts check", "check:docs-links": "tsx .build/check-docs-links.ts", "check:css-vars": "tsx .build/check-css-vars.ts", + "check:compat": "tsx .build/check-compat.ts", "check:open-source": "tsx .build/check-open-source.ts", "check:html": "tsx .build/check-html.ts", "check:markup-classes": "tsx .build/check-markup-classes.ts",