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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f74cd7b0ad4bac887960143bb1fa720c2ac8fa165fde108991f168698374b66a
4
- data.tar.gz: f513ea9081c3b2ddb1201a9dcdad3ec5cb8bf8c564df781b831ce8810ba69b19
3
+ metadata.gz: 279d52ef4d637a808e0b7f40ad6d253024e57002e1e5a68b9263869da7dfb654
4
+ data.tar.gz: ada97b528fa3846a6699ebaad0af3ca6e980a4b9c635820ff6ba1d769c061882
5
5
  SHA512:
6
- metadata.gz: f8f0d58251a6c8db879034f350a97201e95eaaeb1d66191ce32d0db6c7ac0f656ea9241d2b426a297932d51ea70d04f8040ed5cc58be1da5a8cec85cbcd8cdfc
7
- data.tar.gz: a8701ecf294bbd1652f4d37e0aecf6365992127c09115bb81b13370e6a8f7b4bc9d657dffe263abe7e08072226902647264030332d22205b281a0df36b863a89
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 the Parapet project (a Rails development tool),
10
- > which is its first consumer. Expect names and APIs to change before 1.0. See [CHANGELOG.md](CHANGELOG.md).
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: "#overlay-root" # overlays render here; "" = rules scoped to the canvas only
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
- (a simple `#id`), or `portal` behind the framework prefix (`qss-portal`) or your `id_prefix` (`app-portal`); an id that
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,