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.
data/QSS-MANIFESTO.md CHANGED
@@ -9,12 +9,24 @@ Traditional CSS treats the web as a "Document Stream" where elements flow like i
9
9
  **QSS rejects flow.** It treats the screen as a 1000-unit 2D CAD canvas. Every element has a mathematical origin and a predictable footprint.
10
10
 
11
11
  ### 2. The Deterministic Stack
12
- To ensure isolation and predictable rendering, QSS requires four things:
12
+ To ensure isolation and predictable rendering, QSS requires three things:
13
13
 
14
14
  1. **Stage (`#qss-canvas`)**: The relative-positioned root. All spatial math is relative to this origin. Its id is fixed, so a document has exactly one top-level canvas; nested independent instances belong in separate documents (an iframe), and QSS does not build support for them.
15
- 2. **Wrapper (`#qss-wrapper`)**: Enforces viewport boundaries and provides a "Safety Gutter."
16
- 3. **Noun (`id="..."`)**: The unique Identity. Every "Object" on the canvas is a Noun defined by its ID.
17
- 4. **Actor (`.qss`)**: The mandatory base class (The DNA). It enforces `position: absolute` and a "Nuclear Reset."
15
+ 2. **Noun (`id="..."`)**: The unique Identity. Every "Object" on the canvas is a Noun defined by its ID.
16
+ 3. **Actor (`.qss`)**: The mandatory base class (The DNA). It enforces `position: absolute` and a "Nuclear Reset."
17
+
18
+ A fourth piece, a `#qss-wrapper` enforcing viewport boundaries with a "Safety Gutter," was dropped from this list
19
+ 2026-11-02: it never had an implementing rule, and the gutter it described is incompatible with QSS's full-bleed,
20
+ no-visible-gutter design. See `docs/DESIGN_HISTORY.md` section 15.
21
+
22
+ **Containment constraint.** Every Actor is also `container-type: inline-size`, for the container-query engine that
23
+ resolves `--qnt`. Per the CSS Containment spec, that implies inline-size containment, which requires the browser to
24
+ compute the Actor's own size as if it had no content -- sizing cannot depend on descendants, because that is exactly
25
+ what would make container queries circular. In practice: **no Actor may ever rely on shrink-to-fit/content-based
26
+ `auto` sizing, in either axis**, in any current or future browser that implements container queries correctly. An
27
+ Actor's width and height must always come from an explicit class; a width- or height-less Actor collapses to the
28
+ Nuclear Reset's `min-width`/`min-height` floor instead of sizing to its content. See `docs/DESIGN_HISTORY.md`
29
+ section 16.
18
30
 
19
31
  ### 3. Grammar: The "Adjective" System
20
32
  Adjectives define the traits, positioning, and state of a Noun.
data/README.md CHANGED
@@ -1,183 +1,79 @@
1
1
  # QSS: Quantized Spatial Styling
2
2
 
3
- QSS is a CSS layout framework built so that both people and AI agents can read a layout from its source, without
4
- rendering it. Layout is placed and sized on a 1000-unit grid across a **canvas** element: one **qnt** is one
5
- thousandth of the canvas width, computed by CSS alone. QSS ships compiled CSS, its SCSS sources, an optional
6
- JavaScript engine, and two **auditors** that check views against QSS's rules. It includes a Rails engine for the
7
- asset pipeline; the auditors and configuration also work in plain Ruby.
8
-
9
- > **Status: 0.x, early.** QSS was extracted from a Rails development tool,
10
- > which was its first consumer. Expect names and APIs to change before 1.0. See [CHANGELOG.md](CHANGELOG.md).
11
- > Why QSS is designed this way: [docs/DESIGN_HISTORY.md](docs/DESIGN_HISTORY.md).
12
-
13
- ## Vocabulary
14
-
15
- - **Canvas**: the element QSS measures against, `<div id="qss-canvas">` by default.
16
- - **Actor**: an element with class `qss` inside the canvas. Actors declare their size; QSS's reset keeps them from
17
- shrinking or growing implicitly.
18
- - **qnt**: the QSS unit, `canvas width / 1000`. `qss-w[240]` means 240 qnt wide: 24% of the canvas, at any screen size.
19
- - **Noun, adjective, attribute**: an element's `id` is its noun (its identity), its classes are adjectives (its
20
- traits), and `qss-function` / `qss-relation` are QSS attributes. See [QSS-MANIFESTO.md](QSS-MANIFESTO.md).
21
- - **Foreign content**: anything inside class `foreign` (or `qss-payload-override`) is left to ordinary CSS.
22
-
23
- ## Installation (Rails)
3
+ **A CSS layout system built so AI agents — and the humans reviewing their work — never have to guess.**
4
+
5
+ ## The problem
6
+
7
+ Ask an AI coding agent to move a button 10 pixels left. Watch it struggle. CSS gives it five or six different ways
8
+ to control position — margin, padding, a flex order, a grid column, a transform — and nothing says which one is
9
+ actually in charge on this particular page. It guesses, edits the wrong one, and something else quietly shifts. It
10
+ reports the change as done. The page is now broken.
11
+
12
+ ## The fix
13
+
14
+ QSS removes the guessing by removing the agent's choice. Every element's position and size is **one explicit number**, not
15
+ a tug-of-war between competing properties. Those numbers share a single unit — a **qnt**, 1/1000th of the layout's
16
+ own width, computed in pure CSS — so the same edit holds its exact proportion at any screen size: a phone, a
17
+ laptop, a 4K display. No breakpoints. No media queries. No separate rules to keep in sync. Move something once,
18
+ and it stays put, everywhere.
19
+
20
+ ## Why teams adopt it
21
+
22
+ - **One number, not a tug-of-war.** Position and size are each a single explicit value; nothing else can quietly
23
+ override them.
24
+ - **Scales without breakpoints.** The same markup looks right on a phone and a monitor, because the unit is a
25
+ fraction of the layout's own width, not a fixed pixel count.
26
+ - **Proof, not vibes.** Two built-in auditors flag violations automatically (RED is broken, ORANGE is a judgment
27
+ call), and a real-headless-browser render check confirms the compiled CSS actually renders as intended — a
28
+ passing lint rule isn't the same as watching it work.
29
+ - **Works with zero JavaScript.** The compiled CSS alone is fully functional. An optional JS engine adds a few
30
+ runtime conveniences on top; it's never required.
31
+ - **Plays nice with your design system.** QSS only ever owns position, size and structure. Tailwind or anything
32
+ else still owns color, type and visual polish — and the auditor flags it if a content framework strays into
33
+ QSS's territory.
34
+ - **Built full-bleed by default.** Apps that must fill any real window shape, at any size, with zero per-project
35
+ configuration — no letterboxed gutters, no guessing which breakpoint you're in.
36
+ - **Battle-tested, not theoretical.** Extracted from a real production Rails tool that still runs on it today.
37
+ - **Transparent by design.** Every major decision — including the ones that didn't work — is recorded in
38
+ [docs/DESIGN_HISTORY.md](docs/DESIGN_HISTORY.md), so you can see exactly *why* QSS looks the way it does, not
39
+ just that it does.
40
+
41
+ > **Status: 0.x, early.** Expect names and APIs to change before 1.0. See [CHANGELOG.md](CHANGELOG.md).
42
+
43
+ ## Quick reference
44
+
45
+ | Term | Meaning |
46
+ |---|---|
47
+ | **Canvas** | The element QSS measures against: `<div id="qss-canvas">`, fixed and singular per document. |
48
+ | **Actor** | Any element with class `qss` inside the canvas (or the overlay portal). Declares its own size; QSS's reset stops it shrinking or growing implicitly. |
49
+ | **qnt** | The QSS unit: `canvas width / 1000`. `qss-w[240]` is 240 qnt wide — 24% of the canvas, at any screen size. |
50
+ | **Noun / Adjective / Attribute** | An element's `id` is its noun (identity); its classes are adjectives (traits); `qss-function`/`qss-relation` are attributes. See [QSS-MANIFESTO.md](QSS-MANIFESTO.md). |
51
+ | **Portal** | `#qss-portal` by default: a fixed, viewport-sized layer outside the canvas, for overlays and viewport-anchored chrome (nav bars, toolbars). |
52
+ | **Foreign content** | Anything inside class `foreign` (or `qss-payload-override`) — left to ordinary CSS entirely. |
53
+
54
+ ## 60-second start
24
55
 
25
56
  ```ruby
26
57
  # Gemfile
27
58
  gem "qss"
28
59
  ```
29
60
 
30
- The gem's Rails engine puts QSS's assets on your asset pipeline (`qss/qss_core.css`, `qss/qss_core.scss`,
31
- `qss/qss-engine.js`).
32
-
33
- ### Markup
34
-
35
- The canvas id is fixed: `#qss-canvas` is not configurable, so a document has exactly one top-level QSS canvas.
36
- Two independent QSS instances nested in the same page would collide on it; isolate them in separate documents (for
37
- example an `<iframe>`) instead. This is a known constraint, not something QSS will build support for.
38
-
39
- An id is `{qualifier}-{baseObject}`: the base object is an HTML tag, the qualifier says which one. A qualifier is
40
- declared once in your registry, recording what it adds to its base. Structural frames (`nav`, `main`, ...) carry
41
- `qss-contain-layout`.
42
-
43
- ```erb
44
- <div id="qss-canvas">
45
- <nav id="main-nav" class="qss qss-relative qss-contain-layout qss-w[200] qss-h-full"> ... </nav>
46
- <section id="content-section" class="qss qss-relative qss-contain-layout qss-w[800] qss-h-full"> ... </section>
47
- </div>
48
- ```
49
-
50
- ```yaml
51
- # config/qss_registry.yml (auditor.registry_path in your config, below)
52
- types:
53
- main-nav:
54
- extends: nav
55
- content-section:
56
- extends: section
57
- ```
58
-
59
- This example audits green with `QSS::QssNativeAuditor` (below).
60
-
61
- ### Stylesheet: neutral defaults
62
-
63
61
  ```erb
62
+ <%# app/views/layouts/application.html.erb %>
64
63
  <%= stylesheet_link_tag "qss/qss_core" %>
65
- ```
66
64
 
67
- ### Stylesheet: with your own settings
68
-
69
- Set your values first, then import core; core's settings are `!default`, so yours win. Put
70
- `QSS.stylesheets_path` on your Sass load path so `qss/qss_core` resolves:
71
-
72
- ```scss
73
- // app/assets/stylesheets/application.scss
74
- @import "qss_variables"; // generated from your config by qss-sync (below)
75
- @import "qss/qss_core";
76
- ```
77
-
78
- ```ruby
79
- Sass.compile("app/assets/stylesheets/application.scss",
80
- load_paths: ["app/assets/stylesheets", QSS.stylesheets_path])
81
- ```
82
-
83
- ### The JavaScript engine (optional)
84
-
85
- The compiled CSS works on its own. The engine adds rules at runtime for bracket classes the compiled CSS does not
86
- cover, exports measured element edges as CSS variables, and names each actor's CSS container after its own id
87
- (`container-name`, so Drywall content inside a frame can use a named container-query variant, such as Tailwind's
88
- `@sm/<frame-name>:`, to react to that specific frame's own width -- DESIGN_HISTORY #12.5). Every actor is already a
89
- size container without the engine (`container-type: inline-size`, a static default); only the name needs an id,
90
- which CSS can't read into an arbitrary property on its own.
91
-
92
- ```erb
93
- <%= javascript_include_tag "qss/qss-engine", defer: true %>
94
- <script>
95
- document.addEventListener("DOMContentLoaded", () => { QSSEngine.startPulse(); QSSEngine.startObserver(); });
96
- </script>
97
- ```
98
-
99
- While developing, `QSSEngine.auditPositioning()` lists every element that is `position: fixed` or `sticky` without QSS
100
- owning it, which no static check can see (a Drywall `fixed` utility only becomes a position once a stylesheet resolves
101
- it). Frames, the overlay portal, `foreign` content and the browser's native top layer are exempt. It runs only when
102
- you call it, and `QSSEngine.setPortal("#qss-overlays")` tells it where your portal root is if it isn't `#qss-portal`.
103
-
104
- ## Configuration
105
-
106
- QSS starts neutral. A project declares its own settings in one YAML file and tells QSS where it is:
107
-
108
- ```ruby
109
- # config/initializers/qss.rb
110
- QSS.configure(root: Rails.root, config: "config/qss_config.yml")
111
- ```
112
-
113
- ```yaml
114
- # config/qss_config.yml -- every key is optional; see the gem's config/grammar.yml for the defaults
115
- config:
116
- portal_root: "#qss-portal" # overlays render here (any one #id; default #qss-portal); "" = rules scoped to the canvas only
117
- palette:
118
- brand-main: "#0f1115"
119
- group_names: [menu] # named groups for group-*:hover style classes
120
- auditor:
121
- id_prefix: "app-" # optional project id prefix
122
- registry_path: "config/qss_registry.yml"
123
- sync_targets:
124
- variables: app/assets/stylesheets/_qss_variables.scss
125
- ```
126
-
127
- Relative paths resolve against the declared `root`, never the working directory.
128
-
129
- ### Generating your SCSS settings
130
-
131
- ```sh
132
- QSS_CONSUMER_CONFIG=config/qss_config.yml bundle exec qss-sync
65
+ <div id="qss-canvas">
66
+ <nav id="main-nav" class="qss qss-contain-layout qss-pos-x[0] qss-w[200] qss-h-full"> ... </nav>
67
+ <section id="content-section" class="qss qss-contain-layout qss-pos-x[200] qss-w[800] qss-h-full"> ... </section>
68
+ </div>
133
69
  ```
134
70
 
135
- This writes your `_qss_variables.scss` (the `sync_targets.variables` path). Without `QSS_CONSUMER_CONFIG` it reads
136
- `config/qss_config.yml` from the current directory.
137
-
138
- ## Auditing views
139
-
140
- ```ruby
141
- result = QSS::QssNativeAuditor.audit_file("app/views/layouts/application.html.erb")
142
- result[:status] # => "green", "orange" or "red"
143
- result[:issues] # => [{ type:, message:, severity: }, ...] (plus code: on most identity and unit findings)
144
- ```
71
+ That's a complete, working QSS layout: no JavaScript, no media queries, no breakpoints. `main-nav` sits flush
72
+ against the canvas's left edge and is exactly 20% of its width; `content-section` starts exactly where `main-nav`
73
+ ends and fills the remaining 80% — at any screen size, from a phone to a 4K display.
145
74
 
146
- RED means a rule is violated: fix the markup, or reconsider the rule. ORANGE is a judgment call. Examples of rules:
147
- every actor declares its width and height (or opts out with `qss-content-driven`); structure carries no raw CSS
148
- lengths (sizes are in qnt or canvas percentages); no `!important`; ids follow the `{qualifier}-{baseObject}`
149
- grammar; every class literally resolves to a rule in the compiled stylesheet, not just a known shape; a `qss`-marked
150
- actor carries no class (from a content framework such as Tailwind) that resolves, in `auditor.drywall_stylesheet_path`,
151
- to a rule setting a structural property (`display`, `position`, sizing, `flex*`/`grid*`, `gap`, `z-index`, `order`)
152
- -- checked by CSS property, so it works for any content framework with no fork.
153
- `QSS::QssAuditor` is the older class-whitelist auditor: it predates the bracket vocabulary (it reports `qss-w[200]`
154
- as unknown) and is kept for projects that still use its whitelist.
155
-
156
- **Three tiers of stylesheet.** Class resolution reads core's own `qss_core.css` (always, verified against the manifest
157
- shipped with the gem, `QSS::CoreIntegrityError` on a mismatch), any sealed tool stylesheets
158
- (`auditor.tool_stylesheets`, sealed by the tool's build with `qss-seal`, `QSS::ToolIntegrityError`), and the host's
159
- own output (`auditor.host_stylesheets`, `stylesheet_path`, `QSS.configure(host_stylesheets: -> { ... })`; a Rails host
160
- defaults to `app/assets/builds/*.css`).
161
-
162
- Appearance classes (colors, type styling, shadows, filters) are **deprecated** as of 0.2.0: the auditor reports each as
163
- orange `deprecated_decorative`, since appearance belongs to whatever fills Drywall; QSS's quantized bracket scale
164
- (`qss-border[2]`, `qss-text-size[14]`) is unaffected. A Drywall stylesheet compiled with the `qss` prefix is a red
165
- `drywall_prefix_collision`. Frames stay out of `z-index` entirely.
166
-
167
- **Overlays: use the native top layer.** The portal root is the element whose id is your configured `portal_root`
168
- (any one `#id` selector; `#qss-portal` when none is set), matched exactly: no prefix or substring guessing.
169
- Core gives it its own geometry (a fixed, viewport-sized layer, `pointer-events: none` on the root and `auto` on its
170
- direct children, `--qnt` resolved on its children as on the canvas), so write no QSS classes on the root itself, which
171
- match nothing there, and place it after the canvas in the document so it paints above it without a `z-index`. A modal, dialog, menu or popover belongs outside the canvas, in the overlay
172
- portal root, and should be a `<dialog>` opened with `showModal()` or carry the `popover` attribute: the browser then
173
- renders it above everything with no `z-index` at all. A plain element placed directly in the portal root is the
174
- fallback path. It is allowed, but the auditor reports it as orange `top_layer_preferred` until you mark it
175
- `qss-overlay-fallback`. A `z-index` is permitted only on the portal root and inside it (to order one fallback overlay
176
- above another, or by DOM order alone); anywhere else Portal Integrity makes it red.
177
-
178
- Every QSS class is prefixed `qss-` (SETTLED 2026-10-17, DESIGN_HISTORY #12): QSS owns structure/positioning
179
- exclusively, and any content framework sharing the same canvas (Tailwind or otherwise) must not be able to mistake
180
- its own unprefixed utility classes for QSS's.
75
+ For configuration, the optional JS engine, the two auditors, and the overlay/persistent-chrome conventions, see
76
+ **[docs/HOWTO.md](docs/HOWTO.md)** — the full step-by-step guide.
181
77
 
182
78
  ## Requirements
183
79
 
@@ -60,6 +60,10 @@ const QSSEngine = (function() {
60
60
  'qss-cursor-pointer',
61
61
  'qss-cursor-row-resize',
62
62
  'qss-dev-border',
63
+ 'qss-elastic-y',
64
+ 'qss-elastic-y-max',
65
+ 'qss-elastic-y-min',
66
+ 'qss-elastic-y-pct',
63
67
  'qss-ergonomic-override',
64
68
  'qss-evenly',
65
69
  'qss-fixed',
@@ -333,14 +337,20 @@ const QSSEngine = (function() {
333
337
  * needs an id, which CSS can't read into an arbitrary property on its own. Skips an actor whose container-name
334
338
  * is already set by anything (a future static/declarative path), checked by computed value so an inline style
335
339
  * or any stylesheet rule both count, not just a prior run of this same function.
340
+ *
341
+ * Covers both the canvas and the overlay portal root (DESIGN_HISTORY #17.3): the portal is not a secondary
342
+ * concern bolted onto the canvas, it is where an entire layout's viewport-anchored chrome lives, so its actors
343
+ * need the same named container-query support canvas frames get. Found missing 2026-11-02 while verifying the
344
+ * chrome-docking pattern -- it had simply never been extended there, not a deliberate canvas-only scope.
336
345
  */
337
346
  function applyContainerNames() {
338
- const anchor = document.querySelector(anchorSelector);
339
- if (!anchor) return;
340
- anchor.querySelectorAll('.qss[id]').forEach(actor => {
341
- if (getComputedStyle(actor).containerName === 'none') {
342
- actor.style.containerName = actor.id;
343
- }
347
+ [ document.querySelector(anchorSelector), document.querySelector(portalSelector) ].forEach(root => {
348
+ if (!root) return;
349
+ root.querySelectorAll('.qss[id]').forEach(actor => {
350
+ if (getComputedStyle(actor).containerName === 'none') {
351
+ actor.style.containerName = actor.id;
352
+ }
353
+ });
344
354
  });
345
355
  }
346
356