Changelog
English · 简体中文
0.7.0 — 2026-08-11
JSON configuration
- The CLI now accepts
adaptive.config.json, strips its$schemametadata before compilation, and reports malformed or non-object JSON without exposing an internal stack trace. - The published JSON Schema describes
$schema, so editors can complete and validate a JSON configuration against the same option model used by the documentation site. - JSON files are read with
readFileandJSON.parse, preserving the package's Node 18 support; JavaScript configuration remains available for regular expressions and predicate functions.
Quality gates
- Added ESLint, Prettier, VitePress source type-checking, documentation-site builds, and stricter coverage thresholds to
npm run checkand CI. - Added a ratio-based performance budget that compares compiler cost with PostCSS parse-and-print cost, avoiding unstable absolute timing limits on shared runners.
- Expanded evaluator, selector scanner, CLI, schema, stdin, compatibility and error-path coverage; coverage now reaches 98.10% statements, 93.59% branches and 99.35% lines.
- Fixed two issues found by the new checks: serialising a value-parser node through an unsupported path and allowing an unknown value to stringify as
[object Object]in generated output.
0.6.0 — 2026-08-11
Breakpoints have a canvas now
- New
mediaroute matcher. A responsive stylesheet is one file holding two design files: the phone numbers were measured on a 750 mock and the ones inside@media (min-width: 1024px)on a 1440 one. Nothing in the CSS says so, and until now nothing could.routes: [{ media: { minWidth: 1024 }, profile: 'pc' }]gives the breakpoint the design file it was drawn on. - This was not a near miss, which is why it is worth a feature rather than a note.
@media (min-width: 1024px) { .hero { padding: 40px } }on a 750 canvas bounded at 600px compiled toclamp(17.07px, 5.33vw, 32px)— and that rule is only ever live from 1024px up, past where the canvas stops scaling, so theclamp()was already pinned to its maximum everywhere the rule applies. The padding was a constant 32px at every width, forever. The compiler ran, the output looked compiled, and not one value moved. - Matching is by implication rather than by text.
{ minWidth: 1024 }claims any rule that cannot apply below 1024px, soscreen and (min-width: 1200px)and(min-width: 1024px) and (max-width: 1600px)are both claimed, and nesting composes because nesting is conjunction. Comparing the params as a string would have missed thescreen and— and a design canvas is a fact about which widths a rule reaches, not about how the query was spelled. remandemin a media query resolve at 16px, not atrootValueand not at the root element's font size. A query is evaluated before any declaration could changefont-size, so it cannot depend on the cascade it selects:64remis 1024px even in a stylesheet whosehtmlis62.5%. This is a fact about the query rather than an assumption about the page, and reading onlypxwould have left every Tailwind and UnoCSS project — which write every breakpoint inrem— unreadable. The continuity check reads the same parser and gained the same reach.- A query the compiler cannot read — a comma,
not,only, or any non-width feature — is claimed by nothing. That is a refusal, not a "matches everything": routing a rule on a condition nobody verified is how a canvas mistake gets made rather than caught.@containernever counts either; it bounds an element, andvwhas never been about the element. - A
selectorroute still outranks a width band, because a component library is drawn on its own canvas at every viewport width and a breakpoint does not change which design file a component came from. A route may name both —{ selector: ['.van-'], media: { minWidth: 1024 }, profile: 'pc' }— which is how "this component is redrawn at the desktop breakpoint" is said. - An empty band (
{}) and a reversed one (minWidth: 1024, maxWidth: 600) are configuration errors that throw. The first matches every rule in the stylesheet, which is a slow way of changingdefaultProfile; the second matches nothing. Both read as working configuration, so neither may be left to be discovered from the output.
The dead-band warning
- You do not have to know any of the above to find the problem. When a rule converts a length and the band it is live in lies entirely outside its canvas's fluid range, the compiler now says so, names both ranges, and gives the route that fixes it. This is arithmetic rather than a heuristic — two intervals that do not overlap — and it is the failure the multi-canvas model exists to prevent, arriving through the one door the model never watched.
- Reported once per canvas per band per file, and only for rules that actually converted something. A breakpoint that changes
displayandcoloris ordinary CSS with no lengths to be constant about, and warning on those would bury the report in the places it does not apply. The first version did exactly that, which is what the test for it was written from. - The suggested fix adapts: when a selector route chose the canvas, the message asks for
{ selector: […], media: … }, because a bare media route loses to a selector route and would have changed nothing. Advice that does not work is worse than no advice. appPcPresetnow reads its own breakpoint as well as writing it. The preset already said "the desktop design file takes over at 768px" — that number set each profile'squery, so an@adaptive pcblock came out wrapped correctly. It just never read a media query, so a hand-written@media (min-width: 768px)block compiled against the phone canvas: the preset disagreeing with itself. Both directions are now routed, and themax-widthhalf is stated explicitly rather than left todefaultProfile, so the pair keeps meaning what it says if the preset is spread over a desktop-first configuration.- Two conformance cases,
breakpoints/media-routeandbreakpoints/dead-band, covering what is claimed, what is refused, and the one-report-per-band rule.
Selector routing
- Fixed
:not()and:has()arguments deciding which canvas a rule lands on..page-hero:not(.van-cell)styles page elements — precisely the ones that are not Vant cells — and was being sent to Vant's 375 canvas because the substring.van-cellappeared somewhere in the text. Every length in such a rule came out at exactly twice its intended size, silently. Routing now reads the subject of the selector: the arguments of:not()and:has()name an element other than the one being styled and are removed before matching, while:is()/:where()/:matches()/-*-any()arguments are kept, since those genuinely are alternatives for the subject. Measured against 22,761 rules in 11 published component-library stylesheets, 1,035 of which contain:not()or:has(): zero change canvas, because library CSS always carries its own prefix outside the exclusion. The fix costs nothing and closes a hole that application code walks into. :is()and:where()lists spanning two canvases now warn. This was previously documented as undetectable.:is(.van-cell, .page-hero) { padding: 16px }is one declaration wanted on two canvases, the same problem as a comma-separated list one bracket deeper, and it was going through unreported.- The warning states what splitting would cost, computed rather than left to the reader:
:is()matches every branch at the specificity of its highest one, so pulling the branches apart is free only when they already agree. When they do not, the warning names the drop —:is() matches every branch at its highest, 1-1-0, so ".page-hero" would drop to 0-1-0. This is what makes the advice actionable; a warning whose fix silently changes the cascade is a warning people learn to ignore. One report per rule: the remaining lists have the same cause and the same fix. - New
src/core/selectors.ts, dependency-free:splitSelectorList,routingSelector,nestedSelectorLists,specificity,compareSpecificity,formatSpecificity,splitIsSpecificityNeutral. Splitting is string-aware and attribute-aware, so[data-x="a)b"], .csplits into two — naive bracket counting gets that wrong. Specificity follows Selectors Level 4, including:where()counting zero,:is()/:not()/:has()taking their highest branch,:nth-child(n of S)counting as a pseudo-class plus the highest ofS, and the four legacy single-colon pseudo-elements (:before,:after,:first-line,:first-letter) counting as elements. 40 new unit tests.
Browser support audit
:has()is now audited. It is the newest selector in everyday use and the four engines are years apart on it — Chrome 105 (Aug 2022), Safari 15.4, and Firefox not until 121 (Dec 2023). That spread is the widest in the table, and the failure is selector-level: the whole rule is dropped. A stylesheet reviewed in Chrome and Safari can be silently missing rules in an older Firefox. The compiler emits no:has(); it arrives from your own CSS or a component library and survives the pass, which is exactly why an audit that reads the output sees it. Unlike native nesting it is unmistakable in the text, so it is reported rather than guessed at.css-hasadded toscripts/capture-compat.mjsand regenerated intosrc/core/compat-data.ts; still no runtime dependency.
A documentation site
- The documentation is published as a site, in both languages, at
https://moresyl.github.io/postcss-adaptive-matrix/. It is built out of the repository rather than out of a copy of it:srcDiris the repository root, sodocs/README.mdlinking to../conformance/README.mdreaches a real page here for the same reason it does on GitHub, and no file moved. Search, dark mode, an edit link and a last-updated stamp come with it. - The one thing that had to be designed rather than configured is the language switcher. VitePress resolves a relative link against the page's rewritten location, so
[English](./README.md)at the top of a Chinese page — correct on GitHub — resolves back to the Chinese page on the site. Links are therefore resolved in the repository, against the source path, and emitted as absolute site URLs. Moving a page cannot break them. - Every option is published as data at
/schema/options.json: a JSON Schema 2020-12 document with each option's type, permitted values, range and default. Prose is the wrong shape for "isprecisionan integer and what is its ceiling". - Two things stop it being decoration. Its property tables are typed over the source interfaces, so an option that exists but is not described fails
tsc, and so does a described option that no longer exists. And every default is read out ofresolveOptions()when the file is generated rather than transcribed, so a default that changes in the code changes here in the same commit. A test closes the gap the type system cannot see — that the default printed in the configuration reference is that same default — and found one disagreement immediately:unitToConvertresolves to a list, and both reference tables said'px'. - Both languages travel inside the one schema:
descriptionis English,x-description-zhis Chinese. A type is not a translation.x-alsonames the JavaScript-only forms — aRegExp, a predicate function — that JSON cannot express but a config file accepts. - The compiler runs in the reader's browser. The plugin has one runtime dependency and no Node API in its path, so the published source is imported straight into the playground page and PostCSS runs client-side. There is no service behind it and nothing to keep in sync with a release. The options pane evaluates as a JavaScript expression rather than parsing as JSON, because half of what is worth trying cannot be written in JSON. Six samples, one per question people actually ask, including a configuration the compiler warns about.
llms.txtandllms-full.txtper language, following the llms.txt convention, and the raw Markdown of every page served at the page's path plus.md— so "give this page to a model" is a fetch rather than a scrape of rendered HTML. Three buttons above the outline use it: copy as Markdown, view raw, and open the page in a conversation.- New
docs/agents.md/docs/agents.zh-CN.mdgathers those endpoints in one place, including what belongs in a prompt: an agent that assumes one global design width writes a configuration that compiles and is wrong. - Deployed by
.github/workflows/docs.ymlon push tomain.tsconfig.jsonnow coversdocs/.vitepress, so the site's own source is type-checked with everything else.
Documentation
- New
scripts/check-docs.mjs, wired intonpm run checkand therefore into CI. It verifies that every local link resolves, that every#anchormatches a real heading, and that every page has its counterpart in the other language and links to it near the top. The pairing check is the one that earns its keep: the two languages are written separately rather than translated, which is what makes them read properly and also what lets one of them quietly not exist. It found two —CODE_OF_CONDUCThad only a Chinese version under an English filename, and the example README had both languages stacked in one file. - The anchor check splits on
/\r?\n/. On Windows every file here is CRLF, and a trailing\rdefeats$in a non-multiline pattern while also not being matched by.— so a naive split finds no headings at all and reports every anchor in the repository as broken. That is not hypothetical; it is what the first version of this check did. examples/app-pcgainedadaptive.config.mjs, exporting the options alone;postcss.config.mjsnow imports them. The two runners want different shapes — PostCSS wants{ plugins: [...] }, the CLI wants the options — and writing the canvases out twice is how the two end up describing different designs. The example READMEs now give a command that actually runs, including--targets.
Performance
routingSelectorreturns immediately for a selector with no:in it, which is most of them, and the reduction now happens insideforSelector— after the check that skips selector routing entirely. A project withlibraries: falseno longer pays anything at all for the feature. Compiler time on the utility-framework corpus: 4.38ms → 3.58ms.
Conformance
- The atomic fixtures gained
space-x-4anddivide-y-2, which had left the suite with no functional pseudo-classes at all despite being everyday utilities. One utility, three unrelated real shapes: Tailwind 4 wraps the whole thing in:where(...), UnoCSS wind3 writes a flat> :not([hidden]) ~ :not([hidden]), and UnoCSS wind4 emits native nesting with&. All three now produce an identicalclamp(1.70667px, 0.53333vw, 2.56px)for the same 2px, theborderhairline survives in all three, and wind4's native nesting is written back unchanged. - New
test/idempotence.test.ts: compiling compiled output changes nothing, across nine configurations × eight stylesheets. The conformance suite already asserted this per fixture, but only under the options that fixture declares — what was uncovered is the cross product of the switches that change the shape of the output, and the shape is what a second pass has to survive. A second pass is not hypothetical: a package that ships pre-compiled CSS goes through the consuming application's pipeline again, and so does a monorepo that compiles a shared component library and then the app importing it. - The case that wanted pinning down is atomic mode with static text. Atomic mode adds
remtounitToConvert, and text is normally written asrem + vw— thevwis what tells a second pass the value is already compiled. WithfontFluidity: 0there is novw:32pxbecomes a bare2rem, which the next pass reads as a design length and converts again. It survives, but nothing defends it —rootValueis used at both ends, so writing ÷16 and reading ×16 are exact inverses and the value is its own fixed point. That is a property of the arithmetic rather than a rule anyone wrote down, and if either end of that division moves the failure is silent: no error, no warning, just text a little smaller on every save in a watch loop.
Types
- Every array-valued option now accepts a
readonlyarray:routes,libraries,textProperties,propList,selectorExclude,valueExclude,include,exclude,root.injectTo, and thefile/selector/property/prefix/tokenPrefixfields of routes and library definitions.unitToConvertalready did, which made the API inconsistent: a configuration written withas const— the natural way to write one in TypeScript — type-errored on every field but that one.
0.5.0 — 2026-08-10
Documentation
- Every documentation file is now bilingual:
X.mdin English,X.zh-CN.mdin Chinese, cross-linked at the top of each page. That covers the README, all 11 docs pages, the conformance suite description, the contributing guide, the security policy and this file. The English is not a machine translation of the Chinese — the same point wants different sentences in each language, so the two versions were written separately. - The English artwork lives in
docs/assets/en/and was redrawn for English: Chinese labels are short, so substituting the text would overflow the cards, and the dashed plain-vw line would run undertracks the viewport. - The canvas-model diagram no longer names third-party libraries. The model has nothing to do with whose library it is; "a mobile component library / 375 design file" and "a desktop component library / no design file · real pixels" say exactly the same thing, and the diagram is about this compiler from start to finish.
Browser support audit and degradation
- New
auditCompatibility(css, targets): give it "browser + the oldest version you support" and it lists every piece of syntax in the output beyond your targets, what is lost when it is unsupported, and the switch that turns it off. The audit reads the compiled stylesheet text rather than the configuration — the only way audit and output can never drift apart, and it sees features that arrive via a preset, via a component-library route or via hand-written CSS alike. Also exported:detectFeatures,COMPAT_FEATURES,FEATURE_SUPPORT,compatFeature, fully typed. - New CLI flag
--targets "safari 14, ios_saf 13", printing aneedssection after the comparison. The order of those lines is deliberate: what is lost first, what to switch to second — "iOS Safari 13 is too old" is not actionable on its own, and what has always mattered about a CSS support gap is how much goes with it. CSS does not error, it discards: an unreadable value takes its declaration, an unreadable selector takes its rule, an unreadable at-rule takes its whole block, all silently. So the feature table is ordered by how much is lost, not by which feature is newest. - Covers 11 features:
@layer,:where(),@container/ container units,clamp()/min()/max(),vi, logical properties,var(),env(safe-area-inset-*),vw, and native nesting. The compiler does not emit native nesting (nesting it reads is written back as it was); it is listed because it is a question that gets asked, and because pattern-matching it would misread ordinary CSS as nested — a false report is worse than no report. - Version data is baked into
src/core/compat-data.tsfrom caniuse-lite byscripts/capture-compat.mjs. caniuse-lite stays a devDependency, the plugin adds no runtime dependency, and the audit works offline. "Which version a feature became usable in" is history and will not change again; usage share is deliberately not consulted — whether 0.4% of users count is a decision about your project, andbrowserslistis already the place that answers it. The scan takes the version from which support was never lost again, not the first version showing ay(a few features shipped and were then withdrawn). --targetstakes explicit names and versions, not a browserslist query: a query would pull in the browserslist package, answers a question about your users rather than about this stylesheet, and the same query changes meaning as the database updates — not one character of code changes and next month the build goes red. An unrecognised target name is an error that exits1rather than being skipped silently: a target quietly dropped is worse than no audit, because it reads like a pass.- caniuse has no separate entries for
:where()orvi, so the:is()andsvh/lvh/dvhentries are used, marked in the data as proxies rather than measurements. Both pairs come from the same section of the specification and shipped together (:where()/:is(): Chrome 88, Firefox 78, Safari 14). - New
root.logical(preset fieldrootLogical): set tofalse, the foundation writeswidth/margin-left/margin-right/max-widthinstead ofinline-size/margin-inline/max-inline-size. This is a real gap found while building the audit — logical properties are the only syntax the compiler emits whose failure still leaves a page that looks fine: withoutmargin-inline: autothe column is exactly the right width, sitting against the left edge of the screen; withoutmax-inline-sizeit goes full-bleed. Neither looks like a fault, so it is likelier to ship than a visible collapse, and until now no switch avoided it. The two spellings are equivalent on a horizontal page, so switching costs nothing. The preset also gainedrootLayer, passed through tolayer— also a support switch, and reaching it should not cost you the preset. - New documentation page: Browser support and degradation — the feature × minimum-version matrix, a per-feature "what emits it / what is lost / how to switch it off and what that costs", and the boundary of what this audit cannot replace (it only proves your target browsers can parse the syntax in this CSS; rendering differences, keyboards and address bars are different problems). Conversely, a real device cannot test a version threshold either — the iOS 17 in your hand reads
@layer, which says nothing about 15.4 and below.
Breakpoint check
- Fixed two
shrinksfalse positives that fired reliably on the default preset with no configuration at all:root.fixedContainingBlockrewrites a fixed element'sleft: 0intoleft: var(--adaptive-root-gutter), and that gutter is meant to step at a breakpoint — as the column goes from 480 to 1920, the gutter correctly drops from 143.96px to 0. Read as a design length that is a regression; read as itself it is the correction working. The variable is now substituted with0pxbefore comparison rather than skipping the group entirely, so theclamp()inleft: calc(clamp(…) + var(--adaptive-root-gutter))is still checked. A check that shouts on the default configuration is a check people learn to skip.
Atomic CSS (Tailwind / UnoCSS)
- Fixed no utility class being converted at all, silently: hand-written CSS scaled, utilities did not, and the two size systems drifted apart with no error. Each major version was blocked in a different place. The older ones (Tailwind 3, UnoCSS
presetUno/presetWind3) write lengths inremwhile the compiler only readpx; the newer ones (Tailwind 4, UnoCSSpresetWind4) moved lengths entirely into theme tokens, so.p-4compiles topadding: calc(var(--spacing) * 4)— there is no length in the utility to read, and custom properties are not converted by default. unitToConvertnow accepts an array, reading several units in one pass. This is not a nicety: both frameworks emit both units in the same stylesheet — spacing and font sizes inrem, border widths and bracketed arbitrary values likep-[13px]inpx, all describing the same design file. Reading onlyremmisses every border, reading onlypxmisses every gap, and both single choices are wrong.rembecomes pixels viarootValue; every other unit is read at face value.- Fixed
unitToConvert: 'rem'having always treated1.5remas 1.5 pixels: with no conversion before comparison it fell below theminPixelValueandhairlinethresholds and whole stylesheets were skipped. Both thresholds are now judged in pixels —0.0625remand1pxare the same hairline, and how you spell it does not change how thin it is. - New
rootValue(default 16), one ruler shared by both ends: it decides how many pixels areminput is worth, and how manyrema text size's static part is written as. A project withhtml { font-size: 62.5% }setsrootValue: 10. - New
withAtomicCss(base, options?): wraps rather than replaces an existing configuration, addingremtounitToConvertand claiming the theme token prefixes--spacing,--text-,--leading-,--radius-,--container-. Claiming the source is enough and the utilities need no changes —calc(clamp(a, b, c) * 4)is identicallyclamp(4a, 4b, 4c)(multiplication by a positive coefficient passes through a clamp), and the output is digit-for-digit identical to converting16pxdirectly. Three families are deliberately not claimed:--breakpoint-*is the width at which a canvas switches, so scaling it moves the breakpoint itself;--tracking-*is published inem, and the font size it hangs off is already fluid, so scaling again compounds it;--shadow-*pixels are depth drawn at screen scale. Add your own length families withtokenPrefixes. - The default
textPropertiesnow includes--text-*and--leading-*: when a font size is published as a token its name does not look like a font property, and missing it means that font size loses browser zoom. This only decides how an already-converting length is written, not whether it converts, so it does nothing for an unclaimed token. - New conformance cases
atomic/{tailwind-v4,unocss-wind3,unocss-wind4}, whose input is the actual published output of all three (Tailwind 4.3.3 and two UnoCSS 66.7.5 presets), re-capturable withscripts/capture-atomic.mjs. The two major-version shapes differ far too much for a hand-written copy to be anything but what you imagined. Neither framework is a devDependency: the captured CSS is the entire input, and tyingnpm testto someone else's release schedule buys no extra information. - A regular expression passed to a route's
propertychannel now throws aTypeErrorimmediately, explaining the correct form. The other two channels accept regexes; this one takes string prefixes only, and the previous behaviour was a mid-runprefix.toLowerCase is not a function.
Breaking (types): ResolvedAdaptiveMatrixOptions.unitToConvert changes from string to string[]; AdaptiveMatrixOptions.unitToConvert widens to string | readonly string[], so passing a string still works. findContinuityIssues gained an optional second parameter, rootFontSize.
Compiler
- Fixed cross-canvas text sizing: when the project canvas and a library canvas differ, all of that library's text was wrong. Ordinary lengths always agreed (both reduce to
value ÷ canvas); text did not — text keeps a fixedremcomponent so browser zoom stays effective, and that fixed length used to anchor to each canvas separately. With Vant on 375 and the page on 750 the two describe the same design in two sets of units, so Vant's 16px and the page's 32px are the same size — yet at a 390px viewport they rendered as 16.22px and 26.62px, about 40% too small, and invisible at both 375 and 1440. A 750-file project using Vant is one of the most common mobile combinations there is; antd-mobile's 1x/2x pair of artifacts is the same situation. - New profile field
textAnchorWidth: the width the static part of text anchors to, defaulting todesignWidth. A library canvas always inherits the anchor of the profile it belongs to, so there is nothing to configure. The equivalent form is "convert the length into the anchor canvas's units first, then apply the formula as usual": the fluid term is exactly unchanged (P × F / Dcancels proportionally with the canvas), and only the static term is normalised. For non-text lengthsfontFluidity = 1and the static term is always 0, so the output is byte-for-byte unchanged, andstrategy: 'viewport'is unaffected too. - New property test: "the same design on a different canvas is the same size". Across random canvases and random scale factors,
V px on DandV×k px on D×kmust agree at every viewport. The conformance suite can only cover canvas combinations someone thought to write down, and this bug only surfaced when two canvases were present at once.
Compiler and validation
@adaptive <canvas>landing on a profile with noquerynow warns. Projects split by folder (src/mobile/**andsrc/pc/**, one page tree each) usually have noqueryon either profile — switching is not CSS's job there. Writing@adaptive pc { ... }in a shared component then reads as "these rules are for desktop", but compiles to unconditional rules that come later in the file and therefore win at every viewport, with nothing anywhere saying so.query: falsedoes not warn: that is an author stating explicitly that switching happens outside CSS. An@adaptivepointing at its own canvas does not warn either: there is no switch, and unwrapping loses nothing.- New documentation section, "two page trees, split by folder", stating that file routing only decides which design file to convert against and never adds a media query, plus the trade-offs around shared components, component libraries and route specificity.
Build tool integration
- New real Vite build test: the scaffold has a
postcss.config.mjsthat Vite discovers for itself, a dependency stylesheet imported fromnode_modules, and a<style>block supplied the way@vitejs/plugin-vuesupplies one. Until now nothing verified a single claim in the integration documentation — and when those claims stop holding, the build still succeeds and only the stylesheet is wrong. Now verified: the config is found and applied; a dependency lands on the component-library canvas via itsnode_modulespath with output byte-for-byte identical to the equivalent size on the page; an SFC id with a query string is matched by a contains-stylefileroute; and a second build produces identical output. - The documented trap "an end-anchored regex never matches an SFC" is now a test too: building with
/\.mobile\.css$/asserts that the<style>block really is left silently on the default canvas while the rest of the output is unchanged. A claim like that only counts once it has been verified from the other side.
Optional runtime
- Fixed a frame leak in
observeAdaptiveViewport:update()is a public method, yet it cleared the scheduler's frame handle. Callingupdate()manually and thendestroy()left an already-queued frame neither recorded nor cancelled, so it landed on a destroyed observer on the next tick. Only the scheduler clears the handle now,destroy()zeroes it, and a repeateddestroy()no longer cancels a handle the host has already recycled and reissued. - Filled in the runtime tests: the fallback for old WebViews with no
visualViewport, negative keyboard heights from iOS rubber-band scrolling, per-field non-numeric readings, event coalescing, destroy timing, and reading the globals when called with no arguments — the path every browser user actually takes. Branch coverage 72% → 100%.
Component libraries
- New
scripts/verify-libraries.ts: downloads each built-in library's published artifact, compiles it with a realisticnode_modulespath, and checks whether the prefix really exists, which canvas the route lands on, whether the result is idempotent, whether there are warnings, and whether the seam check produces a false positive. Until now every registry entry except Vant was written from documentation with no evidence; the three items below were all found by this script. - Fixed antd-mobile: the library publishes the same stylesheet twice,
bundle/drawn on 375 and2x/bundle/on 750, with identical class and token names (measured on 5.42.3, every length in the latter exactly double the former). The.adm-prefix route used to convert the 2x artifact against 375, making every size on the page exactly twice what it should be, with no error and no warning. Newantd-mobile-2xentry, active in automatic mode with nothing to configure. - New
scoped: restrictsprefixandtokenPrefixto only count whenfilematches too. When one prefix maps to two canvases, only the path tells them apart. Path-scoped routes are tested before unscoped ones because they are more specific; when the path does not exist (a bundler inlined the dependency), it falls back to the unscoped one.scopedwithoutfileis an error. - Removed Varlet's
tokenPrefix: '--var-': that library's custom properties carry no prefix at all — they are--field-padding,--icon-size-md, declared on a bare:root, and the rule matched nothing. Claiming those names means claiming--card-widthitself, and the registry only takes unambiguous prefixes, so nothing was added; the documentation shows the explicit route instead. - The documentation now carries measured prefix hit rates, and states that the design width column cannot be checked (a stylesheet does not reveal how wide the file it was drawn on was), and that
naive-uiandmuigenerate their styles at runtime with no stylesheet on disk.
Breakpoint seam check
- Only reports when at least one side is a compiler-produced formula. It used to read a library's own deliberate breakpoint difference as a seam: Quasar's
.q-tooltipispadding: 8px 16pxon a phone and6px 10pxabove 600px, a tap-target trade-off where both numbers were written and compared by a person. The completeness argument "a regression can only come from a canvas change" covers only formulas this compiler produced, and does not hold for a stylesheet left as authored. One side converted and the other not still reports. - Conformance cases now use compiler-shaped values. Over a dozen negative cases used a bare
40px, which after this gate would pass because they were never compiled rather than because of the thing each was meant to verify.
0.4.0 — 2026-08-09
Breakpoint seam check
- New breakpoint seam check: the CLI points out declarations where widening the viewport makes a size smaller, with the actual pixel values on both sides. Two design files can each be right and still disagree at the seam, and this class of problem only appears at one width — the 375 and 1440 you debug at every day are both fine. The check is also exported as
findContinuityIssues(root)so a build can fail on it. - The comparison is on absolute value, and a sign change across the breakpoint is never reported. The absolute value of every formula the compiler emits is non-decreasing in viewport width and never changes sign, so an absolute-value regression can only come from a canvas change across a breakpoint. Comparing signed numbers was inverted for every negative length: negative margins and bleeds grow by moving away from zero.
- The check substitutes theme tokens from the same stylesheet. Component libraries almost never write literal sizes — of Vant 4.10.0's 3198 ordinary declarations, 1173 read entirely through
var(), so giving up at the firstvar(means avoiding precisely the layer this plugin adapts. Substitution happens only when the value is decided by viewport width alone: the token must be declared only on:root/:host/htmlwith no second copy elsewhere, and every declaration must be either unconditional or inside a pure pixel-width@media. A token rewritten at a breakpoint is itself a seam, even if the rule consuming it is written once. Measured (the complete Vant 4.10.0 stylesheet): evaluable value components 622 → 1309 (17.6% → 36.9%), with 779 tokens collected. - Measured false positives: across the 69 conformance fixtures, the Vant stylesheet above, and this repository's example project, the
shrinkscount is 0 everywhere. - New
evaluateLength: evaluates the compiler'sclamp()/min()/max()/calc()at a given viewport width.env(),%and container units returnnullrather than a guessed number.
Packaging and types
- Fixed the CommonJS entry:
require('postcss-adaptive-matrix')returned a namespace object, so calling it directly threwplugin is not a functionand.defaultwas mandatory. Butplugins: [require('postcss-adaptive-matrix')({ ... })]is the universal shape of everypostcss.config.js— this repository's own Webpack documentation example was wrong.module.exportsis now the plugin itself, with.defaultand the named exports still available as properties. - Fixed the CommonJS type entry, in two places:
typesinexportsexisted only at the top level, so CJS consumers got the ESM.d.ts; and.d.ctsitself only declared named exports, soimport x = require('postcss-adaptive-matrix')reported "has no call signatures" — code that ran while the editor showed red.requirenow points at.d.cts, which describes the real shape withexport =, types are preserved through a merged namespace, andimport type { AdaptiveMatrixOptions }still works. The ESM entry and its types are unaffected. - New tests against the built artifact itself: every other test imports from
src, but whatrequirereceives and which file types resolve to are decided by the build and bypackage.json, and importing from source can never test that.npm run checktherefore builds before testing.
Compiler and validation
- A selector list that straddles canvases (
.van-cell, .page-hero { ... }) now warns and names the selector that lost. One declaration can only have one answer, and the previous behaviour silently compiled the whole rule on the first canvas that matched. @adaptive pc;(no block) used to be rewritten as@media (min-width: 768px);— not valid CSS, while the rules the author meant to put on that canvas stayed on the original one. It now warns and is left as written.- The CLI's
-cno longer falls back silently to the built-in defaults when the config module is missingdefaultor the preset was never called. Both used to run to completion and print a comparison that looked correct, with nothing anywhere saying the configuration had not been read. - New configuration validation: the values of
unitandstrategy, an emptyunitToConvert, anatRuleNamecolliding with an at-rule CSS already defines, and an emptyroot.selector. Getting any of these wrong fails silently, and a wrongunitemits outright invalid CSS. - New property-based random tests: using
evaluateLengthas the oracle, design-width identity, absolute-value monotonicity, constancy outside the range, linearity inside it and idempotence are verified across hundreds of generated canvases. The conformance suite can only cover design widths someone thought to write down.
0.3.0
- New
adaptive-matrixCLI preview: a per-declaration before/after, with--fromto verify file routing before you ship and--cssto emit the full output while warnings go to stderr. - Fixed nesting: declarations inside
@media/@supports/@layer/@container/@scope/@starting-styleare now converted, while lengths inside@font-face/@page/@property/@counter-styleare left as written. - Fixed idempotence:
clamp()/min()/max()that already carry a viewport unit are no longer converted a second time. - Fixed idempotence of the root foundation: the output carries a
/* postcss-adaptive-matrix foundation */marker, so recompiling neither scales a fixed ceiling likemax-inline-size: 480pxas if it were a design-file size, nor stacks on a second copy. - Ignore comments (the
adaptive-ignorefamily) are no longer removed from the output. An ignored value carries no trace of itself, so once the comment is gone a second pass converts the size the author explicitly excluded; minifiers remove the comments, so shipped size is unaffected. - The conformance suite gained an idempotence assertion for every case: recompiling the output must return it unchanged.
- Fixed duplicate declarations: every repetition of a declaration within one rule is now converted. Only the first used to be, while the cascade uses the last — so the conversion was effectively lost.
- Numbers with exponents are supported:
1e2pxis 100px, which used to be skipped silently, andmin(1e2vw, 50px)is no longer misjudged as having no viewport unit. - Fixed at-rule casing:
@ADAPTIVE/@Adaptiveare equivalent to@adaptive, matching CSS's case-insensitivity for at-keywords. They used to go unrecognised, and the browser discarded the whole block with no indication. - New
root.injectTo(rootInjectToon the preset): limits which files receive the root foundation. In a component-based project every<style>block is a separate file, so the default injects one copy into each. - An exclude-only
propList(such as['!border*']) is now an error — it matches no property, which means the entire stylesheet goes unconverted. - Using the reserved
library:prefix inprofilesis now an error pointing back atlibraries: [{ extends }]— it used to be silently overwritten by the synthesised library canvas. - The unknown-canvas warning no longer lists internally synthesised library canvases, and explains that the browser discards the whole at-rule; under
unknownProfile: 'error'the error no longer suggests enabling an option that is already enabled. - New registry invariant tests, covering prefix ambiguity, canvas values and the short-prefix policy in automatic mode.
- Restructured the documentation, adding getting started, build tool integration, component libraries, optional runtime and CLI preview, with SVG diagrams.
0.2.0
- Added component-library canvases, automatic recognition, route overrides and design-token adaptation.
- Added the fixed root containing block correction and desktop offset handling.
- Decoupled the compilation core from the PostCSS adapter layer, exposing the resolved multi-canvas configuration.
- Established 50+ conformance fixtures, 133 tests and a benchmarking tool.
- Rewrote the multi-canvas model, component library, architecture and migration documentation.
0.1.0
- First implementation of the app/desktop multi-canvas compilation model.
- Bounded fluid sizing, zoomable text, and media-query and container-query profiles.
- Dynamic design widths, file/property/selector/value filters and ignore directives.
- Optional root layout, safe-area variables and the VisualViewport runtime.
- Published ESM, CommonJS and TypeScript types.