qss 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +345 -175
- data/QSS-MANIFESTO.md +16 -4
- data/README.md +62 -166
- data/app/assets/javascripts/qss/qss-engine.js +16 -6
- data/app/assets/stylesheets/qss/qss_core.css +12024 -0
- data/app/assets/stylesheets/qss/qss_core.scss +15 -0
- data/config/grammar.yml +6 -0
- data/config/integrity.json +2 -2
- data/docs/DESIGN_HISTORY.md +882 -0
- data/docs/HOWTO.md +269 -0
- data/exe/qss-core-render-check +38 -0
- data/exe/qss-engine-render-check +8 -2
- data/lib/qss/qss_auditor.rb +5 -1
- data/lib/qss/version.rb +1 -1
- metadata +12 -8
data/CHANGELOG.md
CHANGED
|
@@ -3,124 +3,165 @@
|
|
|
3
3
|
All notable changes to the qss gem. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
4
4
|
versions follow [Semantic Versioning](https://semver.org/) (before 1.0, minor versions may break the API).
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
Each entry below is written from that release's own vantage point — what problem prompted it, what was tried and
|
|
7
|
+
discarded along the way, and what shipped instead — rather than edited later with knowledge of what came after. A
|
|
8
|
+
rejected approach stays rejected in the entry that rejected it, even where a later entry finds a different answer
|
|
9
|
+
to the same question. For the same story told forward in time, with the full unabridged reasoning, see
|
|
10
|
+
[docs/DESIGN_HISTORY.md](docs/DESIGN_HISTORY.md); this file stays to the point a release note needs.
|
|
11
|
+
|
|
12
|
+
## Origins (before 0.1.0)
|
|
13
|
+
|
|
14
|
+
QSS began inside Parapet, a Rails development tool, after a recurring failure: asked to move a sidebar 10px left,
|
|
15
|
+
an AI coding agent would edit a margin, then an `order`, then a `flex-basis` — and something else on the page
|
|
16
|
+
would move. It would report the change as done while the page was visibly broken. The diagnosis mattered: this
|
|
17
|
+
wasn't agents being bad at arithmetic (a single explicit value is easy to edit correctly), it was *ambiguous
|
|
18
|
+
intent-to-mechanism mapping* — "left" is not one property under flex or grid, so the agent has to guess which of
|
|
19
|
+
several plausible ones to touch, and the guess is where it goes wrong. The fix that followed: keep every
|
|
20
|
+
structural value explicit, single-purpose and directly addressable, so an edit always has exactly one target.
|
|
21
|
+
|
|
22
|
+
The first attempt at that fix was a custom bracket grammar (`pos-x[500]`, `w[240]`) with its own JavaScript parser
|
|
23
|
+
and a whitelist auditor. It didn't hold up: the hand-maintained parser was the actual bug farm (a regex one comma
|
|
24
|
+
could defeat, a whitelist that drifted between client and server), an invented syntax cost agents an unfamiliarity
|
|
25
|
+
tax they didn't pay with plain CSS, and "verified" by the tool that made the change turned out not to mean
|
|
26
|
+
verified at all — repeatedly, a change shipped green with the auditor and tests passing while the page didn't
|
|
27
|
+
actually work, and only a real browser ever caught it. The rebuild that followed kept the structural ideas that
|
|
28
|
+
had held up (one flat canvas, frames that isolate their own layout, overlays routed to a portal instead of a
|
|
29
|
+
`z-index` war, no `!important`) and moved everything else to plain CSS custom properties and real browser
|
|
30
|
+
verification. Extracted as this gem once that rebuild stabilized.
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
- The `qss-text-size[N]` floor drops from a flat 9px to 5px (`text_scale.floor_px`). With 9px, every N from 1 to 10
|
|
18
|
-
computed `N * 0.875` below the floor, so `[5]`, `[7]` and `[10]` all rendered at exactly 9px. Now `[5]` floors at 5px
|
|
19
|
-
and `[7]` and up scale with the canvas (`N * 0.875` is the floor from N = 6), so small sizes are distinct. The cap and
|
|
20
|
-
`floor_multiplier` are unchanged. A small size on a narrow canvas can now render smaller than before; the auditor's
|
|
21
|
-
legibility check (below 11, `qss-legibility-override` to waive) is unchanged.
|
|
22
|
-
|
|
23
|
-
## [0.4.2] - 2026-10-20
|
|
32
|
+
## [0.1.0] - 2026-09-25
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
- A dimension can be declared by an `#id { ... }` rule in a configured tool or host stylesheet, not only by a size
|
|
27
|
-
class. The implicit-dimension check (red for a `qss` actor, orange for other structural elements) now also reads the
|
|
28
|
-
actor's own id from `tool_stylesheets`, `host_stylesheets` and `stylesheet_path`: `width`/`inline-size` declares the
|
|
29
|
-
width, `height`/`block-size` the height, `left`+`right` (or `inset`, `inset-inline`) pin the width, `top`+`bottom`
|
|
30
|
-
(or `inset`, `inset-block`) pin the height. This is how an actor anchors to another actor's resizable dimension with
|
|
31
|
-
plain custom properties (`right: var(--bar-w)`), which the bracket classes cannot express. Only unconditional rules
|
|
32
|
-
count: a rule inside `@media`/`@supports`, a nested rule, or a compound selector (`div#x`, `#x:hover`, `#x.on`)
|
|
33
|
-
declares the size only sometimes and is ignored, as is a value that sizes nothing (`auto`, `fit-content`, ...).
|
|
34
|
+
First release, extracted from the Parapet project.
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
The canvas unit (`--qnt`, canvas width / 1000) had spent its whole history being *set by JavaScript* after
|
|
37
|
+
measuring the page — until the day before this release, when it moved to pure CSS (`0.1cqw`, a container query
|
|
38
|
+
unit, registered as a length so it resolves once at the canvas and inherits unchanged). That one change is why the
|
|
39
|
+
compiled stylesheet works with no JavaScript engine at all: previously, the base actor reset outranked every
|
|
40
|
+
compiled utility on CSS specificity, and utilities had only ever worked because the runtime engine re-emitted them
|
|
41
|
+
to win the fight. A render check (headless Chrome, CSS alone, no engine) exists specifically so that fact can
|
|
42
|
+
never go unnoticed again. Also fixed on the way here: an actor's scroll area silently growing to its content's
|
|
43
|
+
full height because nothing required a declared height (now an auditor rule), and the vocabulary settling on
|
|
44
|
+
noun/adjective/attribute (an id is the noun; classes are adjectives; `qss-function`/`qss-relation` are attributes).
|
|
36
45
|
|
|
37
46
|
### Added
|
|
38
|
-
- `
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
- Core stylesheet (`qss/qss_core.scss`, compiled `qss/qss_core.css`): utilities on a 1000-unit canvas grid; the unit
|
|
48
|
+
(`--qnt`, canvas width / 1000) computed by CSS alone (`0.1cqw` of the canvas; the viewport width outside one).
|
|
49
|
+
- Base reset for QSS actors at the canvas id's weight only, so every core utility applies without the JS engine.
|
|
50
|
+
- Neutral `!default` SCSS settings (`qss/_qss_defaults.scss`); a project sets its own before importing core.
|
|
51
|
+
- Optional runtime engine (`qss/qss-engine.js`): rules for uncovered bracket classes, measured element edges.
|
|
52
|
+
- `QSS::QssNativeAuditor`: id grammar (`{qualifier}-{baseObject}`, `QSSObject` custom types) with a per-project
|
|
53
|
+
registry, `qss-function` / `qss-relation` attributes, explicit dimensions for every actor, unit rules (no raw CSS
|
|
54
|
+
lengths on QSS structure; rem or % only in foreign content), `!important`, z-index and inline-style checks.
|
|
55
|
+
- `QSS::QssAuditor`: the older class-whitelist auditor.
|
|
56
|
+
- `QSS.configure(root:, config:)`: a project's settings read at runtime; paths resolve against its root.
|
|
57
|
+
- `QSS::Blueprints` (`QSS.grid`, `QSS.object`): layout arithmetic and object blueprints.
|
|
58
|
+
- `QSS::Engine`: Rails engine for the asset pipeline, loaded only when Rails is present.
|
|
59
|
+
- `qss-sync`: writes a project's SCSS settings (`--core` regenerates the gem's own generated sections).
|
|
60
|
+
- `qss-core-render-check`: renders the compiled CSS alone in headless Chrome and checks core utilities.
|
|
41
61
|
|
|
42
|
-
|
|
43
|
-
- A configured stylesheet that is not on disk (`drywall_stylesheet_path`, `stylesheet_path`, or a literal `host_stylesheets` entry)
|
|
44
|
-
now raises `QSS::ConfigurationError` naming the file. Before, it loaded as an empty stylesheet, so a Tailwind build that had
|
|
45
|
-
not run yet showed up as a large, misleading `unresolved_class` count. A glob that matches nothing is still just empty.
|
|
62
|
+
## [0.1.1] - 2026-10-05
|
|
46
63
|
|
|
47
|
-
|
|
64
|
+
### Fixed
|
|
65
|
+
- `contain-layout` and `qss-contain-layout` now have a rule (`contain: layout`) in `qss_core.css`. The README
|
|
66
|
+
documents the class for structural frames and the native auditor mandates it, but the stylesheet never defined it,
|
|
67
|
+
so a frame that satisfied the auditor received no containment. Anyone already using the class will now get layout
|
|
68
|
+
containment on those frames: check that absolutely and fixed-positioned descendants still position as expected.
|
|
69
|
+
Covered by a spec on the compiled stylesheet and by three checks in the browser render check (21 in total).
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
- Three-tier stylesheet audit. The auditor now resolves classes against up to three tiers, and tiers 1 and 2 are
|
|
51
|
-
integrity-checked and otherwise silent (they surface only as an integrity failure):
|
|
52
|
-
1. **Core**: the gem's own `qss_core.css`, always known, never configured. `rake compile` marks it DO NOT EDIT and
|
|
53
|
-
writes `config/integrity.json` (its SHA-256, line endings normalized, plus the gem version); the auditor
|
|
54
|
-
verifies it before use. A mismatch, a missing file or a manifest from another version raises
|
|
55
|
-
`QSS::CoreIntegrityError`.
|
|
56
|
-
2. **Tool**: `auditor.tool_stylesheets`, a dev tool's own compiled chrome (for example Parapet's
|
|
57
|
-
`qss_application.css`). The tool seals each file when it builds it with the new `qss-seal` executable (or
|
|
58
|
-
`QSS::Integrity.seal`), which writes `<file>.qss-seal`; `--header` also marks the file DO NOT EDIT. An unsealed or
|
|
59
|
-
changed file raises `QSS::ToolIntegrityError`.
|
|
60
|
-
3. **Host**: `auditor.host_stylesheets`, the end consumer's own compiled output, as paths or globs relative to the
|
|
61
|
-
project root, plus whatever `QSS.configure(host_stylesheets: -> { ... })` returns (evaluated each time the
|
|
62
|
-
settings are read, in the host's own process). With nothing configured, a Rails host gets
|
|
63
|
-
`app/assets/builds/*.css`.
|
|
64
|
-
- `QSS::IntegrityError`, the base of both integrity errors, so a host's CI can rescue and report one class.
|
|
71
|
+
## [0.1.2] - 2026-10-05
|
|
65
72
|
|
|
66
73
|
### Changed
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
check is unchanged and still reads `drywall_stylesheet_path` only.
|
|
71
|
-
|
|
72
|
-
## [0.3.1] - 2026-10-19
|
|
73
|
-
|
|
74
|
-
### Fixed
|
|
75
|
-
- The overlay portal root had no base geometry in the compiled stylesheet, so anything portaled into it rendered at
|
|
76
|
-
the wrong position and size. Every generated utility is scoped `:is(<canvas>, <portal root>) .class`, which only
|
|
77
|
-
ever matches a descendant of a root, never the root itself, so `qss-absolute qss-inset-0` written on the portal
|
|
78
|
-
root itself matched nothing; the canvas escapes this through its own unscoped "Root Anchor" rule, and the portal
|
|
79
|
-
had no equivalent. Measured in a real browser: the root landed in normal flow after the canvas (zero width and
|
|
80
|
-
height, `top` equal to the viewport height), and a `qss-w[640] qss-pos-r[16]` overlay inside it resolved against
|
|
81
|
-
that empty box (negative `left`, wrong size). The portal root now gets its own rule by id: `position: fixed;
|
|
82
|
-
inset: 0` (a viewport-sized layer; placed after the canvas it paints above it with no `z-index`),
|
|
83
|
-
`container-type: inline-size` with `--qnt` resolved on its children exactly as the canvas does (so qnt-sized
|
|
84
|
-
overlays measure against the viewport's width, equal to the canvas unit whenever the canvas spans the viewport),
|
|
85
|
-
`pointer-events: none` on the root and `auto` on its direct children (a viewport-sized transparent layer must not
|
|
86
|
-
swallow clicks meant for the page), and the same actor reset the canvas gives its actors. That reset moved into a
|
|
87
|
-
shared mixin (`qss-actor-reset`), so an actor portaled into an overlay is a full Steel Frame actor too.
|
|
88
|
-
Classes written on the portal root itself still do nothing; its geometry is built in. The engine's runtime rules
|
|
89
|
-
were already unscoped and needed no change.
|
|
90
|
-
- New real-browser coverage in `rake render_check` (`qss-core-render-check`, now 35 checks): a portal root carrying
|
|
91
|
-
the documented `qss-absolute qss-inset-0` is asserted to compute a fixed, exactly viewport-sized box, and a
|
|
92
|
-
qnt-sized overlay inside it to have its real width, height and edge offsets. Run against the 0.3.0 stylesheet, 7 of
|
|
93
|
-
those checks fail, reproducing the report. The static auditor cannot catch this class of bug, since statically the
|
|
94
|
-
classes on the portal root look fine.
|
|
74
|
+
- Gem metadata only, no code changes: the summary and description now explain why QSS exists (agents and flex/grid
|
|
75
|
+
layout, addressable structure, static auditing and its limits, the origin in Parapet), and the gem links to
|
|
76
|
+
`docs/DESIGN_HISTORY.md` as its documentation. The rubygems.org page reads these from the latest release.
|
|
95
77
|
|
|
96
|
-
|
|
97
|
-
- `config.portal_root` now defaults to `#qss-portal` in `config/grammar.yml` (it was empty), so the shipped stylesheet,
|
|
98
|
-
the auditors and the engine agree on one default portal id. The compiled core CSS therefore scopes its utilities to
|
|
99
|
-
`#qss-portal` alongside the canvas and carries the portal rule above. Set it to `""` to build without a portal root.
|
|
78
|
+
## [0.1.3] - 2026-10-17
|
|
100
79
|
|
|
101
|
-
|
|
80
|
+
A last round of structural loose ends closed before the vocabulary itself became the next focus: a size container
|
|
81
|
+
that cannot depend on its content, a 29-percent-dead-class census, an extreme-viewport blind spot, and whether a
|
|
82
|
+
frame's own overflow should be allowed to paint over its neighbors.
|
|
102
83
|
|
|
103
84
|
### Added
|
|
104
|
-
- `
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
85
|
+
- `QSS::QssNativeAuditor::Dimensions.missing_axes` now recognizes two more ways an axis counts as declared,
|
|
86
|
+
revisiting a 2026-09-24 deferral now that flex and Tailwind's boundary are settled (DESIGN_HISTORY #12):
|
|
87
|
+
two opposite insets on the same axis (`pos-x[N]`+`pos-r[N]` for width, `pos-y[N]`+`pos-b[N]` for height -- both
|
|
88
|
+
edges pinned fixes the size exactly with no width/height class at all), and `flex-1`/`flex-auto` on the flex
|
|
89
|
+
parent's own main axis (resolved from the parent's `.vertical`/`.flex`/`.horizontal` classes; with no flex
|
|
90
|
+
parent, or on the cross axis, it still declares nothing -- never guessed). Takes an optional `parent_classes:`
|
|
91
|
+
keyword, passed by `audit_file` from the actual DOM parent.
|
|
92
|
+
- `QSS::QssNativeAuditor` now checks that every class literally resolves to a rule in the compiled stylesheet, not
|
|
93
|
+
just that its shape matches a known pattern. The 2026-09-21 census found about 29 percent of real class tokens
|
|
94
|
+
resolved to no CSS at all while both auditors reported green; this is the guardrail that finding called for,
|
|
95
|
+
closing the gap. Checks core's own `qss_core.css` by default, or a consumer's own compiled output via the new
|
|
96
|
+
`auditor.stylesheet_path` config key (following the same pattern as `registry_path`) -- a consumer should point
|
|
97
|
+
this at what its markup actually renders against. A handful of classes are deliberately CSS-free behavioral
|
|
98
|
+
flags (`qss`, `foreign`, `qss-legibility-override`, `qss-ergonomic-override`, `qss-payload-override`,
|
|
99
|
+
`qss-dampen-scaling`, `qss-content-driven`) and are exempt. New finding: `unresolved_class` (red), deduplicated
|
|
100
|
+
per distinct class per file. Covered by a new spec file (21 examples) plus a configuration spec for the new key.
|
|
112
101
|
|
|
113
102
|
### Changed
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`
|
|
120
|
-
|
|
103
|
+
- `contain-layout`/`qss-contain-layout` now clip paint by default (`contain: layout paint`, was `contain: layout`
|
|
104
|
+
alone), so a frame's overflow can no longer bleed into its neighbours -- `contain: layout` isolates layout math
|
|
105
|
+
but not paint. Anything that needs to visually escape a frame on purpose (tooltips, dropdowns, modals) already
|
|
106
|
+
has to go through the portal (a `z-index` outside it is its own red violation), so this doesn't reopen that
|
|
107
|
+
case. New opt-out for the rare frame that wants decorative overflow (a shadow, a hover scale) without the
|
|
108
|
+
portal: add `qss-paint-override` alongside `contain-layout`/`qss-contain-layout` to restore layout-only
|
|
109
|
+
containment. Covered by two new specs on the compiled stylesheet and, in the browser render check, a real
|
|
110
|
+
`elementFromPoint` hit-test proving an overflowing child is actually clipped (and, with the override, actually
|
|
111
|
+
isn't) -- not just that `getComputedStyle` reports the right `contain` value.
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
- `QSS::QssNativeAuditor`'s advice for an unregistered native qualifier (`{qualifier}-{nativeTag}`) no longer
|
|
115
|
+
suggests dropping the qualifier. Doing so turns the id into a bare native tag, which the auditor itself flags as
|
|
116
|
+
a separate red `missing_qualifier` violation -- the old advice traded one red finding for another instead of
|
|
117
|
+
resolving anything. The only advice now given is to register what the qualifier adds to the base object.
|
|
118
|
+
- `--qnt` (the structural unit) is now dampened at the extremes: `clamp(0.39px, 0.1cqw, 3.2px)` instead of plain
|
|
119
|
+
`0.1cqw`/`0.1vw`. Floor is the qnt value at a 390px CSS viewport width (a real narrow-mobile width regardless of
|
|
120
|
+
the panel's physical/FHD+/QHD+ resolution, since `cqw`/`vw` only ever see CSS pixels); cap is the qnt value at
|
|
121
|
+
3200px, just past a maximized 1440p/27" monitor, so ordinary desktop widths (up to ~2560px) scale exactly as
|
|
122
|
+
before and only genuine ultra-wide/high-resolution canvases or genuine phone-narrow ones are dampened. Past the
|
|
123
|
+
cap, every structural dimension stays fixed regardless of how much wider the canvas gets -- extra width becomes
|
|
124
|
+
more usable space, not bigger borders/gaps/padding. Bounds are configurable (`config.dampen_scale` in
|
|
125
|
+
`grammar.yml`/a consumer's config). Covered by 4 new render-check assertions (25 in total), 2 proving the clamp
|
|
126
|
+
engages at real extreme widths, not just that the unclamped math is right.
|
|
127
|
+
- Responsive percentage utilities (`qss-{breakpoint}:w-pct[N]`, `h-pct`, `pos-x-pct`, `pos-y-pct`, for every
|
|
128
|
+
horizontal and vertical breakpoint) now compile to valid CSS. The generator previously interpolated the raw
|
|
129
|
+
number (`#{$i * 0.1}%`), which Dart Sass renders with a space before the unit (`width: 0.1 %;`), an invalid
|
|
130
|
+
declaration browsers drop; it now uses `percentage()`, same as the non-responsive `w-pct[N]` rule already did.
|
|
131
|
+
These rules were also never scoped to `#qss-canvas` like every other utility in the file (missing the
|
|
132
|
+
`qss-dsl-boost` wrapper), so once valid they would have applied anywhere a host app reused the class name; they
|
|
133
|
+
are now scoped along with everyone else. Covered by two new specs on the compiled stylesheet, confirmed to fail
|
|
134
|
+
against the old output and pass against the fix.
|
|
121
135
|
|
|
122
136
|
## [0.2.0] - 2026-10-18
|
|
123
137
|
|
|
138
|
+
The largest decision in the gem's history so far, and a correction rather than new direction: Kraig's original
|
|
139
|
+
intent for QSS, from before any earlier pivot, was never "QSS replaces CSS" — it was QSS for positioning and
|
|
140
|
+
custom element definitions, a real content framework (Tailwind) for standard form elements and common widgets.
|
|
141
|
+
That division was written into Parapet's own project docs but never actually built; over several pivots, the
|
|
142
|
+
hand-authored bracket vocabulary had quietly absorbed the content-styling job too, without anyone deciding that on
|
|
143
|
+
purpose. A direct coverage audit of the compiled stylesheet made the drift concrete: zero `@keyframes`, zero
|
|
144
|
+
gradients, zero native form-control restyling, zero `aspect-ratio`; the pseudo-class/pseudo-element coverage
|
|
145
|
+
committed to a month earlier had never been built; the "a class must resolve to real CSS" guardrail the
|
|
146
|
+
29-percent-dead-class census had called for had never been built either; roughly 32,000 responsive rules were
|
|
147
|
+
still silently emitting invalid CSS.
|
|
148
|
+
|
|
149
|
+
The reasoning that settled it: two different problems had been treated as one. The founding QSS problem — "move X
|
|
150
|
+
left 10px" breaking under flex/grid — is ambiguous intent-to-mechanism mapping, and QSS's `--qnt` grid and bracket
|
|
151
|
+
utilities actually solve that; it stays. Gradients, animations, native form-control restyling and full
|
|
152
|
+
pseudo-class coverage are not ambiguous-mechanism problems at all — each has exactly one correct CSS mechanism —
|
|
153
|
+
they're a *breadth* problem, and hand-authoring breadth inside QSS's own grammar is the one move that had broken
|
|
154
|
+
every single time it was tried here (a generator gap affecting 786 elements, the 29-percent dead-class census, the
|
|
155
|
+
still-open 32,000-rule bug, `contain-layout` shipping with no implementing rule at all in 0.1.1). Scaling the
|
|
156
|
+
hand-authored surface up to match the full CSS spec would bet on the exact mechanism that kept failing, without
|
|
157
|
+
first closing the verification gap every one of those postmortems had already called for. So: QSS's core stays
|
|
158
|
+
scoped to structure, positioning and the native auditor's governance; a real, content-scanning Tailwind build is
|
|
159
|
+
adopted as a dependency for everything else, where that coverage already exists, is battle-tested, and is
|
|
160
|
+
something agents already have deep training exposure to — the same unfamiliarity-tax argument that killed the
|
|
161
|
+
original custom DSL, now applying in Tailwind's favor instead. The one piece that had to be built, not assumed, to
|
|
162
|
+
make this a real boundary rather than a convention someone could forget: a native-auditor rule that a QSS-marked
|
|
163
|
+
actor may never carry a plain content-framework utility that sets a structural property.
|
|
164
|
+
|
|
124
165
|
### Added
|
|
125
166
|
- Every Steel Frame actor (class `qss`) is now its own CSS size container (`container-type: inline-size`, a static
|
|
126
167
|
default in `qss_core.scss`) with a `container-name` derived from its own id, set at runtime by `qss-engine.js`
|
|
@@ -178,8 +219,8 @@ Never published as 0.4.3 or 0.5.0; both numbers were used for this same work bef
|
|
|
178
219
|
`qss-flex-row`/`qss-flex-col`; `spaced` is `qss-justify-between`; `clickable` is `qss-cursor-pointer`; `smooth` is
|
|
179
220
|
`qss-transition-all`. Identity/relational markers that aren't adjectives (`qss`, `group`, `foreign`) are
|
|
180
221
|
unchanged. This restores `QSS-MANIFESTO.md`'s own original grammar (`.qss-pos-x[500]`, `.qss-Button[...]`), which
|
|
181
|
-
had drifted during
|
|
182
|
-
|
|
222
|
+
had drifted during earlier pivots into a mix of bare and `qss-` spellings for the same rule -- a drift one of
|
|
223
|
+
core's own comments had flagged as "prefix decision still open."
|
|
183
224
|
A bare, unprefixed vocabulary this close to a content framework's own utility names (`flex`, `absolute`,
|
|
184
225
|
`w-full`, ...) is the exact collision the boundary exists to prevent: a framework compiling its own unprefixed
|
|
185
226
|
utilities couldn't be told apart, in source text, from QSS's own. `qss-engine.js`'s runtime parsing (already
|
|
@@ -230,92 +271,221 @@ Never published as 0.4.3 or 0.5.0; both numbers were used for this same work bef
|
|
|
230
271
|
whatever text sat between two rules, comment included, as selector text for the rule that followed it. Comments
|
|
231
272
|
are now stripped before extraction in both methods.
|
|
232
273
|
|
|
233
|
-
## [0.
|
|
274
|
+
## [0.3.0] - 2026-10-18
|
|
275
|
+
|
|
276
|
+
Two follow-ups built the same day 0.2.0 shipped, once a real consumer had started migrating to it. First: 0.2.0
|
|
277
|
+
recognized the overlay portal root three different ways (a configured id, the literal `qss-portal`, or `portal`
|
|
278
|
+
behind the project's own id prefix), and every one of those was a convention an auditor had to guess at — one real
|
|
279
|
+
view had an unrelated `customer-portal` nav flagged by the substring match. Second: the static auditor can see a
|
|
280
|
+
Drywall class that sets `position` only when it resolves against the configured Drywall stylesheet on a frame; a
|
|
281
|
+
plain `fixed`/`sticky` utility sitting on any other element is invisible to a class-shape check, because a class
|
|
282
|
+
name only becomes a position once a stylesheet resolves it — something only the browser actually knows.
|
|
234
283
|
|
|
235
284
|
### Added
|
|
236
|
-
- `
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
just that its shape matches a known pattern. The 2026-09-21 census found about 29 percent of real class tokens
|
|
245
|
-
resolved to no CSS at all while both auditors reported green; this is the guardrail that finding called for,
|
|
246
|
-
closing the gap. Checks core's own `qss_core.css` by default, or a consumer's own compiled output via the new
|
|
247
|
-
`auditor.stylesheet_path` config key (following the same pattern as `registry_path`) -- a consumer should point
|
|
248
|
-
this at what its markup actually renders against. A handful of classes are deliberately CSS-free behavioral
|
|
249
|
-
flags (`qss`, `foreign`, `qss-legibility-override`, `qss-ergonomic-override`, `qss-payload-override`,
|
|
250
|
-
`qss-dampen-scaling`, `qss-content-driven`) and are exempt. New finding: `unresolved_class` (red), deduplicated
|
|
251
|
-
per distinct class per file. Covered by a new spec file (21 examples) plus a configuration spec for the new key.
|
|
285
|
+
- `QSSEngine.auditPositioning()`: an opt-in runtime diagnostic (never run by the pulse) that lists every element whose
|
|
286
|
+
computed position is `fixed` or `sticky` and that QSS does not own, and logs one console warning per call (pass
|
|
287
|
+
`{ warn: false }` to silence it). QSS owns positioning, so the only legitimate holders are a Steel Frame actor
|
|
288
|
+
(`qss`, e.g. `qss-fixed`), anything inside the overlay portal root, content inside `foreign`, and an element in the
|
|
289
|
+
browser's native top layer (an open modal `<dialog>`, a shown popover). The static auditor cannot see the rest: a
|
|
290
|
+
Drywall `fixed`/`sticky` utility on an element QSS does not mark only becomes a position once a stylesheet resolves
|
|
291
|
+
it. `QSSEngine.setPortal(selector)` tells it where the portal root is (default `#qss-portal`). Covered by
|
|
292
|
+
`rake engine_check`.
|
|
252
293
|
|
|
253
294
|
### Changed
|
|
254
|
-
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
295
|
+
- The overlay portal root is now recognised by exact id only: the id configured as `config.portal_root`, or
|
|
296
|
+
`qss-portal` when none is configured. `config.portal_root` is validated when the settings load (it must be empty or
|
|
297
|
+
a simple `#id` selector; anything else raises `QSS::ConfigurationError`, and `qss-sync` aborts before generating
|
|
298
|
+
anything). Removed: the 0.2.0 `portal`-behind-the-project-`id_prefix` form (`app-portal`), the implicit
|
|
299
|
+
`qss-portal-*` match, and the older `QssAuditor`'s substring match on `portal`. A project that configures
|
|
300
|
+
`portal_root` to its real element id is unaffected; only one that relied on the implicit matching with
|
|
301
|
+
`portal_root` unset needs to configure it. New `QSS.validate_portal_root!` and `QSS.portal_id?`.
|
|
302
|
+
|
|
303
|
+
## [0.3.1] - 2026-10-19
|
|
304
|
+
|
|
305
|
+
The same consumer's migration immediately found a second, worse gap: a portal root configured per 0.3.0's exact-id
|
|
306
|
+
rule had no actual geometry in the compiled stylesheet at all. Every generated utility is scoped
|
|
307
|
+
`:is(<canvas>, <portal root>) .class`, which can only ever match a *descendant* of a root, never the root itself —
|
|
308
|
+
so `qss-absolute qss-inset-0`, written directly on the portal root, matched nothing there. The canvas had always
|
|
309
|
+
escaped this through its own separate, unscoped "Root Anchor" rule; the portal had no equivalent. Neither auditor
|
|
310
|
+
could see it, because statically the classes on the portal root look completely fine — only measuring a real
|
|
311
|
+
browser revealed the root sitting in normal flow after the canvas with zero width and height, and a
|
|
312
|
+
`qss-w[640] qss-pos-r[16]` overlay inside it resolving its position and size against that empty box.
|
|
263
313
|
|
|
264
314
|
### Fixed
|
|
265
|
-
-
|
|
266
|
-
|
|
267
|
-
a
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
`
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
against the old output and pass against the fix.
|
|
315
|
+
- The overlay portal root had no base geometry in the compiled stylesheet, so anything portaled into it rendered at
|
|
316
|
+
the wrong position and size. Every generated utility is scoped `:is(<canvas>, <portal root>) .class`, which only
|
|
317
|
+
ever matches a descendant of a root, never the root itself, so `qss-absolute qss-inset-0` written on the portal
|
|
318
|
+
root itself matched nothing; the canvas escapes this through its own unscoped "Root Anchor" rule, and the portal
|
|
319
|
+
had no equivalent. Measured in a real browser: the root landed in normal flow after the canvas (zero width and
|
|
320
|
+
height, `top` equal to the viewport height), and a `qss-w[640] qss-pos-r[16]` overlay inside it resolved against
|
|
321
|
+
that empty box (negative `left`, wrong size). The portal root now gets its own rule by id: `position: fixed;
|
|
322
|
+
inset: 0` (a viewport-sized layer; placed after the canvas it paints above it with no `z-index`),
|
|
323
|
+
`container-type: inline-size` with `--qnt` resolved on its children exactly as the canvas does (so qnt-sized
|
|
324
|
+
overlays measure against the viewport's width, equal to the canvas unit whenever the canvas spans the viewport),
|
|
325
|
+
`pointer-events: none` on the root and `auto` on its direct children (a viewport-sized transparent layer must not
|
|
326
|
+
swallow clicks meant for the page), and the same actor reset the canvas gives its actors. That reset moved into a
|
|
327
|
+
shared mixin (`qss-actor-reset`), so an actor portaled into an overlay is a full Steel Frame actor too.
|
|
328
|
+
Classes written on the portal root itself still do nothing; its geometry is built in. The engine's runtime rules
|
|
329
|
+
were already unscoped and needed no change.
|
|
330
|
+
- New real-browser coverage in `rake render_check` (`qss-core-render-check`, now 35 checks): a portal root carrying
|
|
331
|
+
the documented `qss-absolute qss-inset-0` is asserted to compute a fixed, exactly viewport-sized box, and a
|
|
332
|
+
qnt-sized overlay inside it to have its real width, height and edge offsets. Run against the 0.3.0 stylesheet, 7 of
|
|
333
|
+
those checks fail, reproducing the report. The static auditor cannot catch this class of bug, since statically the
|
|
334
|
+
classes on the portal root look fine.
|
|
286
335
|
|
|
287
|
-
|
|
336
|
+
### Changed
|
|
337
|
+
- `config.portal_root` now defaults to `#qss-portal` in `config/grammar.yml` (it was empty), so the shipped stylesheet,
|
|
338
|
+
the auditors and the engine agree on one default portal id. The compiled core CSS therefore scopes its utilities to
|
|
339
|
+
`#qss-portal` alongside the canvas and carries the portal rule above. Set it to `""` to build without a portal root.
|
|
340
|
+
|
|
341
|
+
## [0.4.0] - 2026-10-19
|
|
342
|
+
|
|
343
|
+
### Added
|
|
344
|
+
- Three-tier stylesheet audit. The auditor now resolves classes against up to three tiers, and tiers 1 and 2 are
|
|
345
|
+
integrity-checked and otherwise silent (they surface only as an integrity failure):
|
|
346
|
+
1. **Core**: the gem's own `qss_core.css`, always known, never configured. `rake compile` marks it DO NOT EDIT and
|
|
347
|
+
writes `config/integrity.json` (its SHA-256, line endings normalized, plus the gem version); the auditor
|
|
348
|
+
verifies it before use. A mismatch, a missing file or a manifest from another version raises
|
|
349
|
+
`QSS::CoreIntegrityError`.
|
|
350
|
+
2. **Tool**: `auditor.tool_stylesheets`, a dev tool's own compiled chrome (for example Parapet's
|
|
351
|
+
`qss_application.css`). The tool seals each file when it builds it with the new `qss-seal` executable (or
|
|
352
|
+
`QSS::Integrity.seal`), which writes `<file>.qss-seal`; `--header` also marks the file DO NOT EDIT. An unsealed or
|
|
353
|
+
changed file raises `QSS::ToolIntegrityError`.
|
|
354
|
+
3. **Host**: `auditor.host_stylesheets`, the end consumer's own compiled output, as paths or globs relative to the
|
|
355
|
+
project root, plus whatever `QSS.configure(host_stylesheets: -> { ... })` returns (evaluated each time the
|
|
356
|
+
settings are read, in the host's own process). With nothing configured, a Rails host gets
|
|
357
|
+
`app/assets/builds/*.css`.
|
|
358
|
+
- `QSS::IntegrityError`, the base of both integrity errors, so a host's CI can rescue and report one class.
|
|
288
359
|
|
|
289
360
|
### Changed
|
|
290
|
-
-
|
|
291
|
-
|
|
292
|
-
`
|
|
361
|
+
- Core's classes are now always known to `unresolved_class`. Before, setting `stylesheet_path` replaced core's
|
|
362
|
+
stylesheet, so a consumer that listed only its own file lost every core class. `stylesheet_path` and
|
|
363
|
+
`drywall_stylesheet_path` keep working (`stylesheet_path` counts as a host stylesheet). The Tailwind/QSS boundary
|
|
364
|
+
check is unchanged and still reads `drywall_stylesheet_path` only.
|
|
293
365
|
|
|
294
|
-
## [0.
|
|
366
|
+
## [0.4.1] - 2026-10-20
|
|
295
367
|
|
|
296
|
-
###
|
|
297
|
-
- `
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
containment on those frames: check that absolutely and fixed-positioned descendants still position as expected.
|
|
301
|
-
Covered by a spec on the compiled stylesheet and by three checks in the browser render check (21 in total).
|
|
368
|
+
### Added
|
|
369
|
+
- `QSS::Error`, the base of every failure QSS raises on purpose: `ConfigurationError`, `IntegrityError` (and so
|
|
370
|
+
`CoreIntegrityError`, `ToolIntegrityError`). One `rescue QSS::Error` covers all of them; each means the audit cannot
|
|
371
|
+
run as configured, not that a view has a finding. `ConfigurationError` is not an `IntegrityError`.
|
|
302
372
|
|
|
303
|
-
|
|
373
|
+
### Changed
|
|
374
|
+
- A configured stylesheet that is not on disk (`drywall_stylesheet_path`, `stylesheet_path`, or a literal `host_stylesheets` entry)
|
|
375
|
+
now raises `QSS::ConfigurationError` naming the file. Before, it loaded as an empty stylesheet, so a Tailwind build that had
|
|
376
|
+
not run yet showed up as a large, misleading `unresolved_class` count. A glob that matches nothing is still just empty.
|
|
304
377
|
|
|
305
|
-
|
|
378
|
+
## [0.4.2] - 2026-10-20
|
|
306
379
|
|
|
307
380
|
### Added
|
|
308
|
-
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
381
|
+
- A dimension can be declared by an `#id { ... }` rule in a configured tool or host stylesheet, not only by a size
|
|
382
|
+
class. The implicit-dimension check (red for a `qss` actor, orange for other structural elements) now also reads the
|
|
383
|
+
actor's own id from `tool_stylesheets`, `host_stylesheets` and `stylesheet_path`: `width`/`inline-size` declares the
|
|
384
|
+
width, `height`/`block-size` the height, `left`+`right` (or `inset`, `inset-inline`) pin the width, `top`+`bottom`
|
|
385
|
+
(or `inset`, `inset-block`) pin the height. This is how an actor anchors to another actor's resizable dimension with
|
|
386
|
+
plain custom properties (`right: var(--bar-w)`), which the bracket classes cannot express. Only unconditional rules
|
|
387
|
+
count: a rule inside `@media`/`@supports`, a nested rule, or a compound selector (`div#x`, `#x:hover`, `#x.on`)
|
|
388
|
+
declares the size only sometimes and is ignored, as is a value that sizes nothing (`auto`, `fit-content`, ...).
|
|
389
|
+
|
|
390
|
+
## [0.5.0] - 2026-10-22
|
|
391
|
+
|
|
392
|
+
A consumer (Parapet) set its header title and its sub-title to two different `qss-text-size[N]` values and measured
|
|
393
|
+
both at exactly 9px in a real browser — not a coincidence. `text_scale.floor_px` was a flat 9, and the per-size
|
|
394
|
+
floor is `N * 0.875`, so every N from 1 to 10 computed below 9 and the flat floor won across that entire range:
|
|
395
|
+
`[5]`, `[7]` and `[10]` were indistinguishable from each other.
|
|
396
|
+
|
|
397
|
+
### Changed
|
|
398
|
+
- The `qss-text-size[N]` floor drops from a flat 9px to 5px (`text_scale.floor_px`). With 9px, every N from 1 to 10
|
|
399
|
+
computed `N * 0.875` below the floor, so `[5]`, `[7]` and `[10]` all rendered at exactly 9px. Now `[5]` floors at 5px
|
|
400
|
+
and `[7]` and up scale with the canvas (`N * 0.875` is the floor from N = 6), so small sizes are distinct. The cap and
|
|
401
|
+
`floor_multiplier` are unchanged. A small size on a narrow canvas can now render smaller than before; the auditor's
|
|
402
|
+
legibility check (below 11, `qss-legibility-override` to waive) is unchanged. The 5 is a judgment call, not a
|
|
403
|
+
derived number: a floor and cap are the right guard for text people actually read, but as the next entry found,
|
|
404
|
+
wrong for text that was never meant to be legibility-constrained in the first place.
|
|
405
|
+
|
|
406
|
+
## [0.6.0] - 2026-10-22
|
|
407
|
+
|
|
408
|
+
The same investigation that produced 0.5.0 raised a second question the floor-only fix couldn't answer: a legibility
|
|
409
|
+
clamp is right for body copy, and wrong for a wordmark or display title that should resize like a photo, with no
|
|
410
|
+
floor holding it artificially large on a narrow canvas and no cap holding it artificially small on a wide one.
|
|
411
|
+
Rather than removing the clamp from `qss-text-size[N]` (which every reader of body copy depends on), a second,
|
|
412
|
+
separate class was added for the different use case.
|
|
413
|
+
|
|
414
|
+
### Added
|
|
415
|
+
- `qss-text-size-linear[N]` (N 1 to 96): text that scales in pure proportion to the viewport, `N * 100vw / 1440`, with no
|
|
416
|
+
floor, no cap and no dampening, for cosmetic text (wordmarks, titles) that should resize like a picture. It uses the
|
|
417
|
+
same unit as `qss-icon-size[N]`, so it tracks the viewport width, not the canvas. Body copy should keep using
|
|
418
|
+
`qss-text-size[N]`. The Ruby auditor's below-11 legibility check only reads `qss-text-size[N]`, so it does not apply here.
|
|
419
|
+
The name was a deliberate choice over `qss-display-size` (names the use, not the behavior) and `qss-text-fluid`
|
|
420
|
+
(collides with the CSS community's existing "fluid type," which usually still clamps). Known limitation at this
|
|
421
|
+
point: the unit this class rides on (`--u`, `100vw / 1440`) tracks the real viewport rather than the canvas, which
|
|
422
|
+
is right for a full-width header and wrong for the same text inside a narrower sized container — `--qnt` already
|
|
423
|
+
solves exactly this shape of problem for everything else, defaulting to the viewport and re-resolving via
|
|
424
|
+
container-query units inside a sized container as one variable rather than two parallel systems. The plan at this
|
|
425
|
+
point is for `--u` to learn that same trick in a future release, not to add a second parallel canvas unit.
|
|
426
|
+
|
|
427
|
+
## [0.7.0] - 2026-11-02
|
|
428
|
+
|
|
429
|
+
Two real, independent consumers (Parapet and a second PWA app under development) hit the same root cause from two
|
|
430
|
+
different angles within days of each other: `--qnt` is one scalar, derived from the canvas's *width* alone, used
|
|
431
|
+
for both axes — deliberate, since it's what keeps two elements positioned on different axes holding their
|
|
432
|
+
relative position to each other as the canvas reshapes (the gem's founding guarantee). But a design that uses the
|
|
433
|
+
`y` axis the same way it uses `x` is implicitly assuming a **square** canvas, and nothing enforces that. The
|
|
434
|
+
second app's landing page measured elements rendering 1,408px and 2,048px below the bottom edge of a real
|
|
435
|
+
1,298px-tall browser window — not clipped, rendering entirely outside the visible, `overflow: hidden` canvas —
|
|
436
|
+
and, separately, a logo clipping its own container on every ordinary 16:9/16:10 window because its height came
|
|
437
|
+
from one canvas axis and its width (derived from that
|
|
438
|
+
height via a fixed image aspect ratio) effectively depended on the other.
|
|
439
|
+
|
|
440
|
+
Several plausible-looking fixes were tried and rejected before landing on the real one:
|
|
441
|
+
|
|
442
|
+
- **Splitting `--qnt` into `--qnt-x`/`--qnt-y`.** Makes y-axis positioning finally proportional to the canvas's
|
|
443
|
+
actual height — but breaks the one guarantee the single scalar exists for, reintroducing "which property
|
|
444
|
+
actually moved it" ambiguity inside QSS's own unit system, the exact failure QSS exists to prevent. Proposed and
|
|
445
|
+
rejected twice, independently, by two different people, before either rejection's reasoning reached the other.
|
|
446
|
+
- **A configured canvas aspect ratio with a letterboxed/pillarboxed canvas.** Actually built, as an app-level
|
|
447
|
+
stopgap, then reverted the same day after producing visible pillarboxing on a real ultra-wide window. QSS is
|
|
448
|
+
built primarily for full-bleed, PWA-style apps that must fill every real window at every resolution with zero
|
|
449
|
+
per-project configuration; a visible gutter, on principle, is not an acceptable trade to get there, regardless of
|
|
450
|
+
whether the lock is a fixed square or a configurable ratio.
|
|
451
|
+
- **The mirror image: scale to cover the viewport and crop whatever overflows**, instead of letterboxing. Solves
|
|
452
|
+
the full-bleed requirement, but only for a design with a genuine decorative margin near its edges to sacrifice —
|
|
453
|
+
it's the wrong default for a design where the edges carry load-bearing UI (a nav bar, a file-tree explorer, a
|
|
454
|
+
toolbar) with nothing there to crop.
|
|
455
|
+
- **A static auditor rule bounding position-plus-size against a configured worst-case ratio.** Zero CSS cost, but
|
|
456
|
+
advisory only, with a hard silent cliff at whatever ratio is configured — a window narrower than that stays
|
|
457
|
+
completely unchecked, which is exactly the original failure.
|
|
458
|
+
|
|
459
|
+
What shipped instead: three separate tools, matched to three different kinds of content, rather than one
|
|
460
|
+
canvas-wide policy. Content whose relative position *is* the point (a toolbar's own internal spacing, a tightly
|
|
461
|
+
grouped cluster) keeps today's rigid, canvas-width-relative `--qnt` exactly as it works today — no change, and no
|
|
462
|
+
canvas content should assume otherwise. Content that's allowed to flex gracefully within a bounded range (headroom
|
|
463
|
+
above a cluster, a gap that's more margin than structural relationship) gets a new clamped percentage-of-parent
|
|
464
|
+
mechanism. Content that's genuinely edge-docked and load-bearing (global nav, a sidebar, a toolbar that must never
|
|
465
|
+
crop) gets documented as belonging in the overlay portal rather than on the canvas at all, sized by `--qnt` but
|
|
466
|
+
positioned by real CSS edges.
|
|
467
|
+
|
|
468
|
+
### Added
|
|
469
|
+
- `qss-elastic-y-min[N]` / `qss-elastic-y-pct[N]` / `qss-elastic-y-max[N]` + the bare `qss-elastic-y` trigger class:
|
|
470
|
+
clamped percentage-of-parent spacing for gaps that should flex with their immediate containing block's actual
|
|
471
|
+
rendered size, bounded between a floor and a cap (`top: clamp(min, pct, max)`), rather than holding rigidly
|
|
472
|
+
proportional to the canvas (`qss-pos-y[N]`) or growing unbounded with the parent (`qss-pos-y-pct[N]` alone). Four
|
|
473
|
+
classes rather than one three-parameter bracket, because a bracket with three independent values can't be
|
|
474
|
+
precompiled the way a single-value one can (it would need every min×pct×max combination generated in advance);
|
|
475
|
+
composing single-value classes keeps this working with zero JavaScript, the same idiom the rest of the grammar
|
|
476
|
+
already uses. Stack all four classes on one element. See DESIGN_HISTORY.md #17.2.
|
|
477
|
+
|
|
478
|
+
- The overlay portal is now documented (`docs/HOWTO.md`) as the home for persistent, viewport-anchored chrome
|
|
479
|
+
(global nav, a file-tree explorer, a toolbar pinned to an edge), not only transient overlays: placed by real CSS
|
|
480
|
+
edges, sized by `--qnt` like any other actor, with no second root needed alongside transient overlays, because a
|
|
481
|
+
native top-layer modal always paints above everything regardless of which root anything lives in, and chrome is
|
|
482
|
+
never meant to float above a modal that's taking over the user's attention. The one contract this depends on:
|
|
483
|
+
overlay-mounting code must always append to the portal, never insert at a fixed index, so later-added overlays
|
|
484
|
+
keep painting over earlier, static chrome. See DESIGN_HISTORY.md #17.3.
|
|
485
|
+
|
|
486
|
+
### Fixed
|
|
487
|
+
- The JS engine's `applyContainerNames` now also names actors inside `#qss-portal`, not just `#qss-canvas`. Named
|
|
488
|
+
container-query variants (Tailwind's `@sm/<frame-name>:`) silently never worked on any portal content before this
|
|
489
|
+
— existing overlays included, not just new chrome. Not a deliberate canvas-only design; found while documenting
|
|
490
|
+
the portal as the home for persistent, viewport-anchored chrome (nav bars, toolbars), which made the gap
|
|
491
|
+
impossible to miss. See DESIGN_HISTORY.md #17.3.
|