qss 0.2.0 → 0.3.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 +21 -0
- data/QSS-MANIFESTO.md +1 -1
- data/README.md +13 -5
- data/app/assets/javascripts/qss/qss-engine.js +43 -0
- data/app/assets/stylesheets/qss/qss_core.scss +537 -537
- data/exe/qss-engine-render-check +104 -86
- data/exe/qss-sync +9 -1
- data/lib/qss/configuration.rb +28 -0
- data/lib/qss/qss_auditor.rb +6 -5
- data/lib/qss/qss_native_auditor.rb +3 -9
- data/lib/qss/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 279d52ef4d637a808e0b7f40ad6d253024e57002e1e5a68b9263869da7dfb654
|
|
4
|
+
data.tar.gz: ada97b528fa3846a6699ebaad0af3ca6e980a4b9c635820ff6ba1d769c061882
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a48f94f85b6679f3f8bd2ebf8786706698a7f6a25048a9c2c406dbc2bcb93d3cbfccc6df6d611b5d36d309969b41598ee206b5af648d2e5359705ad9f249944
|
|
7
|
+
data.tar.gz: 7c35658fb519583b99423eea5337522434215930f5ed35e7e315506fcb4e1a8b1ebb2e96773ba132de4d162be77b96bebbfd43f54f2d114960fbf23f3e99680b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ versions follow [Semantic Versioning](https://semver.org/) (before 1.0, minor ve
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-10-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `QSSEngine.auditPositioning()`: an opt-in runtime diagnostic (never run by the pulse) that lists every element whose
|
|
12
|
+
computed position is `fixed` or `sticky` and that QSS does not own, and logs one console warning per call (pass
|
|
13
|
+
`{ warn: false }` to silence it). QSS owns positioning, so the only legitimate holders are a Steel Frame actor
|
|
14
|
+
(`qss`, e.g. `qss-fixed`), anything inside the overlay portal root, content inside `foreign`, and an element in the
|
|
15
|
+
browser's native top layer (an open modal `<dialog>`, a shown popover). The static auditor cannot see the rest: a
|
|
16
|
+
Drywall `fixed`/`sticky` utility on an element QSS does not mark only becomes a position once a stylesheet resolves
|
|
17
|
+
it. `QSSEngine.setPortal(selector)` tells it where the portal root is (default `#qss-portal`). Covered by
|
|
18
|
+
`rake engine_check`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- The overlay portal root is now recognised by exact id only: the id configured as `config.portal_root`, or
|
|
22
|
+
`qss-portal` when none is configured. `config.portal_root` is validated when the settings load (it must be empty or
|
|
23
|
+
a simple `#id` selector; anything else raises `QSS::ConfigurationError`, and `qss-sync` aborts before generating
|
|
24
|
+
anything). Removed: the 0.2.0 `portal`-behind-the-project-`id_prefix` form (`app-portal`), the implicit
|
|
25
|
+
`qss-portal-*` match, and the older `QssAuditor`'s substring match on `portal`. A project that configures
|
|
26
|
+
`portal_root` to its real element id is unaffected; only one that relied on the implicit matching with
|
|
27
|
+
`portal_root` unset needs to configure it. New `QSS.validate_portal_root!` and `QSS.portal_id?`.
|
|
28
|
+
|
|
8
29
|
## [0.2.0] - 2026-10-18
|
|
9
30
|
|
|
10
31
|
### Added
|
data/QSS-MANIFESTO.md
CHANGED
|
@@ -11,7 +11,7 @@ Traditional CSS treats the web as a "Document Stream" where elements flow like i
|
|
|
11
11
|
### 2. The Deterministic Stack
|
|
12
12
|
To ensure isolation and predictable rendering, QSS requires four things:
|
|
13
13
|
|
|
14
|
-
1. **Stage (`#qss-canvas`)**: The relative-positioned root. All spatial math is relative to this origin.
|
|
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
15
|
2. **Wrapper (`#qss-wrapper`)**: Enforces viewport boundaries and provides a "Safety Gutter."
|
|
16
16
|
3. **Noun (`id="..."`)**: The unique Identity. Every "Object" on the canvas is a Noun defined by its ID.
|
|
17
17
|
4. **Actor (`.qss`)**: The mandatory base class (The DNA). It enforces `position: absolute` and a "Nuclear Reset."
|
data/README.md
CHANGED
|
@@ -6,8 +6,8 @@ thousandth of the canvas width, computed by CSS alone. QSS ships compiled CSS, i
|
|
|
6
6
|
JavaScript engine, and two **auditors** that check views against QSS's rules. It includes a Rails engine for the
|
|
7
7
|
asset pipeline; the auditors and configuration also work in plain Ruby.
|
|
8
8
|
|
|
9
|
-
> **Status: 0.x, early.** QSS was extracted from
|
|
10
|
-
> which
|
|
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
11
|
> Why QSS is designed this way: [docs/DESIGN_HISTORY.md](docs/DESIGN_HISTORY.md).
|
|
12
12
|
|
|
13
13
|
## Vocabulary
|
|
@@ -32,6 +32,10 @@ The gem's Rails engine puts QSS's assets on your asset pipeline (`qss/qss_core.c
|
|
|
32
32
|
|
|
33
33
|
### Markup
|
|
34
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
|
+
|
|
35
39
|
An id is `{qualifier}-{baseObject}`: the base object is an HTML tag, the qualifier says which one. A qualifier is
|
|
36
40
|
declared once in your registry, recording what it adds to its base. Structural frames (`nav`, `main`, ...) carry
|
|
37
41
|
`qss-contain-layout`.
|
|
@@ -92,6 +96,11 @@ which CSS can't read into an arbitrary property on its own.
|
|
|
92
96
|
</script>
|
|
93
97
|
```
|
|
94
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
|
+
|
|
95
104
|
## Configuration
|
|
96
105
|
|
|
97
106
|
QSS starts neutral. A project declares its own settings in one YAML file and tells QSS where it is:
|
|
@@ -104,7 +113,7 @@ QSS.configure(root: Rails.root, config: "config/qss_config.yml")
|
|
|
104
113
|
```yaml
|
|
105
114
|
# config/qss_config.yml -- every key is optional; see the gem's config/grammar.yml for the defaults
|
|
106
115
|
config:
|
|
107
|
-
portal_root: "#
|
|
116
|
+
portal_root: "#qss-portal" # overlays render here (any one #id; default #qss-portal); "" = rules scoped to the canvas only
|
|
108
117
|
palette:
|
|
109
118
|
brand-main: "#0f1115"
|
|
110
119
|
group_names: [menu] # named groups for group-*:hover style classes
|
|
@@ -150,8 +159,7 @@ orange `deprecated_decorative`, since appearance belongs to whatever fills Drywa
|
|
|
150
159
|
`drywall_prefix_collision`. Frames stay out of `z-index` entirely.
|
|
151
160
|
|
|
152
161
|
**Overlays: use the native top layer.** The portal root is the element whose id is your configured `portal_root`
|
|
153
|
-
(
|
|
154
|
-
merely contains the word does not count. A modal, dialog, menu or popover belongs outside the canvas, in the overlay
|
|
162
|
+
(any one `#id` selector; `#qss-portal` when none is set), matched exactly: no prefix or substring guessing. A modal, dialog, menu or popover belongs outside the canvas, in the overlay
|
|
155
163
|
portal root, and should be a `<dialog>` opened with `showModal()` or carry the `popover` attribute: the browser then
|
|
156
164
|
renders it above everything with no `z-index` at all. A plain element placed directly in the portal root is the
|
|
157
165
|
fallback path. It is allowed, but the auditor reports it as orange `top_layer_preferred` until you mark it
|
|
@@ -271,6 +271,7 @@ const QSSEngine = (function() {
|
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
let anchorSelector = '#qss-canvas';
|
|
274
|
+
let portalSelector = '#qss-portal';
|
|
274
275
|
let pulseInterval = null;
|
|
275
276
|
let observer = null;
|
|
276
277
|
let styleTag = null;
|
|
@@ -588,6 +589,46 @@ const QSSEngine = (function() {
|
|
|
588
589
|
anchorSelector = selector;
|
|
589
590
|
}
|
|
590
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Opt-in diagnostic, never run by the pulse: lists every element whose computed position is fixed or sticky
|
|
594
|
+
* that QSS does not own. Positioning is QSS's job, so the only legitimate holders are (1) a Steel Frame actor
|
|
595
|
+
* (class qss, e.g. qss-fixed), (2) anything inside the overlay portal root, (3) content inside a foreign
|
|
596
|
+
* container, and (4) an element in the browser's native top layer (an open modal <dialog>, a shown popover),
|
|
597
|
+
* whose fixed positioning comes from the browser's own stylesheet. Anything else -- typically a Drywall
|
|
598
|
+
* framework's `fixed`/`sticky` utility on an element QSS does not mark -- is a loophole the static auditor cannot
|
|
599
|
+
* see, because a class name only becomes a position once a stylesheet resolves it. Returns the offenders as
|
|
600
|
+
* { element, id, tag, position, classes } and logs one console warning per call unless { warn: false }.
|
|
601
|
+
*/
|
|
602
|
+
function auditPositioning(options = {}) {
|
|
603
|
+
const offenders = [];
|
|
604
|
+
document.querySelectorAll('body *').forEach(el => {
|
|
605
|
+
const position = getComputedStyle(el).position;
|
|
606
|
+
if (position !== 'fixed' && position !== 'sticky') return;
|
|
607
|
+
if (el.classList.contains('qss')) return;
|
|
608
|
+
if (el.closest(portalSelector)) return;
|
|
609
|
+
if (el.closest('.foreign, .qss-payload-override')) return;
|
|
610
|
+
if (inTopLayer(el)) return;
|
|
611
|
+
offenders.push({ element: el, id: el.id || null, tag: el.tagName.toLowerCase(), position, classes: el.className || '' });
|
|
612
|
+
});
|
|
613
|
+
if (options.warn !== false && offenders.length) {
|
|
614
|
+
console.warn(`QSS: ${offenders.length} element(s) use position fixed/sticky outside QSS (frames, the overlay portal, the native top layer):`, offenders);
|
|
615
|
+
}
|
|
616
|
+
return offenders;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function inTopLayer(el) {
|
|
620
|
+
try {
|
|
621
|
+
return el.matches(':modal, :popover-open');
|
|
622
|
+
} catch (e) {
|
|
623
|
+
// A browser without those selectors: treat <dialog> and popover elements as top layer.
|
|
624
|
+
return el.tagName === 'DIALOG' || el.hasAttribute('popover');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function setPortal(selector) {
|
|
629
|
+
portalSelector = selector;
|
|
630
|
+
}
|
|
631
|
+
|
|
591
632
|
/**
|
|
592
633
|
* Registers a reusable spatial object definition
|
|
593
634
|
*/
|
|
@@ -1040,6 +1081,8 @@ const QSSEngine = (function() {
|
|
|
1040
1081
|
return {
|
|
1041
1082
|
transpile,
|
|
1042
1083
|
setAnchor,
|
|
1084
|
+
setPortal,
|
|
1085
|
+
auditPositioning,
|
|
1043
1086
|
registerObject,
|
|
1044
1087
|
calculateGrid,
|
|
1045
1088
|
startPulse,
|