@excom/quark 0.1.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.
- package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +4 -0
- package/language.ts +7 -0
- package/package.json +45 -0
- package/rush-logs/quark.apply-exports.cache.log +1 -0
- package/rush-logs/quark.apply-exports.log +1 -0
- package/rush-logs/quark.build_package-metas.cache.log +1 -0
- package/rush-logs/quark.build_package-metas.log +1 -0
- package/src/actions.ts +445 -0
- package/src/ast.ts +187 -0
- package/src/bindings.ts +115 -0
- package/src/builtin-modules.ts +401 -0
- package/src/constants.ts +47 -0
- package/src/cycle-check.ts +121 -0
- package/src/devtools-hook.ts +303 -0
- package/src/element-api.ts +109 -0
- package/src/evaluator.ts +317 -0
- package/src/form-controls.ts +69 -0
- package/src/language-tables.ts +137 -0
- package/src/language.ts +1225 -0
- package/src/observer.ts +110 -0
- package/src/paint.ts +305 -0
- package/src/properties.ts +818 -0
- package/src/props.ts +94 -0
- package/src/quark-element.ts +7 -0
- package/src/quark-internal.ts +365 -0
- package/src/quark.ts +817 -0
- package/src/resolvers.ts +867 -0
- package/src/rule.ts +800 -0
- package/src/scope-id.ts +47 -0
- package/src/selector-utils.ts +430 -0
- package/src/settle.ts +117 -0
- package/src/types.ts +155 -0
- package/src/utils.ts +79 -0
- package/src/variables.ts +359 -0
- package/support/demos/css-variables.html +18 -0
- package/support/demos/dispatch.html +20 -0
- package/support/demos/events.html +9 -0
- package/support/demos/iterate.html +13 -0
- package/support/demos/js-api.html +17 -0
- package/support/demos/provider-list.html +16 -0
- package/support/demos/toggle-content.html +14 -0
- package/support/demos/unset.html +12 -0
- package/support/demos/view-transition.html +36 -0
- package/support/docs/ATTRIBUTES.md +37 -0
- package/support/docs/AT_RULES.md +45 -0
- package/support/docs/BUILTINS.md +57 -0
- package/support/docs/CONTENT.md +40 -0
- package/support/docs/CSS_VARIABLES.md +16 -0
- package/support/docs/DECLARATIONS.md +26 -0
- package/support/docs/DELAY.md +37 -0
- package/support/docs/DIAGNOSTICS.md +32 -0
- package/support/docs/DISPATCH.md +81 -0
- package/support/docs/ELEMENT_PROPERTIES.md +34 -0
- package/support/docs/EXPRESSIONS.md +31 -0
- package/support/docs/INTERNAL.md +8 -0
- package/support/docs/JS_API.md +53 -0
- package/support/docs/JS_WRITES.md +36 -0
- package/support/docs/LIMITATIONS.md +26 -0
- package/support/docs/LOOP_GUARD.md +30 -0
- package/support/docs/METHODS.md +43 -0
- package/support/docs/MODULES.md +111 -0
- package/support/docs/NO_REVERSION.md +18 -0
- package/support/docs/ON.md +87 -0
- package/support/docs/REACTIVITY.md +24 -0
- package/support/docs/README.md +143 -0
- package/support/docs/SELECTORS.md +87 -0
- package/support/docs/SHEETS.md +67 -0
- package/support/docs/SYNTAX.md +36 -0
- package/support/docs/USE.md +24 -0
- package/support/docs/VALUES.md +27 -0
- package/support/docs/VARIABLES.md +47 -0
- package/support/docs/VIEW_TRANSITION.md +64 -0
- package/support/docs-sections.json +65 -0
- package/support/package-meta.json +208 -0
- package/support/scripts/build-language-docs.mjs +196 -0
- package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
- package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
- package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
- package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
- package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
- package/support/tests/actions.test.ts +437 -0
- package/support/tests/builtin-modules.test.ts +337 -0
- package/support/tests/builtins.test.ts +189 -0
- package/support/tests/css-variables.view.test.ts +35 -0
- package/support/tests/delay.test.ts +244 -0
- package/support/tests/devtools-hook.test.ts +496 -0
- package/support/tests/diagnostics.test.ts +224 -0
- package/support/tests/dispatch.view.test.ts +43 -0
- package/support/tests/element-api.test.ts +202 -0
- package/support/tests/evaluator.test.ts +237 -0
- package/support/tests/events.view.test.ts +40 -0
- package/support/tests/form-controls.test.ts +452 -0
- package/support/tests/helpers.ts +354 -0
- package/support/tests/internals.test.ts +568 -0
- package/support/tests/iterate.view.test.ts +40 -0
- package/support/tests/js-api.view.test.ts +45 -0
- package/support/tests/language-docs.test.ts +147 -0
- package/support/tests/lifecycle.test.ts +440 -0
- package/support/tests/loop-guard.test.ts +323 -0
- package/support/tests/provider-list.view.test.ts +47 -0
- package/support/tests/quark-features.test.ts +2887 -0
- package/support/tests/quark-with-providers.test.ts +169 -0
- package/support/tests/quark.test.ts +359 -0
- package/support/tests/resolvers.test.ts +346 -0
- package/support/tests/selector-analysis.test.ts +281 -0
- package/support/tests/selectors.test.ts +670 -0
- package/support/tests/toggle-content.view.test.ts +40 -0
- package/support/tests/unset.view.test.ts +42 -0
- package/support/tests/view-helpers.ts +81 -0
- package/support/tests/view-transition.test.ts +1069 -0
- package/support/tests/view-transition.view.test.ts +76 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Limitations
|
|
2
|
+
|
|
3
|
+
What the current engine does not observe or support, and the pitfalls that follow from it.
|
|
4
|
+
|
|
5
|
+
## Beta limits
|
|
6
|
+
|
|
7
|
+
- Selector features backed by state the DOM does not reflect are not observed: interaction and validity pseudo-classes (`:hover`, `:focus`, `:checked`, `:invalid`, …) match on the first run only, and the rule logs a warning at build. Everything else the native engine matches is observed — see [Selectors](./SELECTORS.md) for the list and the partial cases (`:empty` ignores text-only changes, `:nth-child(… of S)` observes only the attributes in `S`, `:open` covers `<details>` / `<dialog>` only, sibling-relative `:has(+ …)` re-runs the whole rule)
|
|
8
|
+
- At-rules are Quark's own — `@use`, `@scope`, `@on`, `@dispatch` / `@command`, `@view-transition`, `@delay`, `@warn` / `@debug` / `@error` — and any other name is a parse error, so CSS's `@media` / `@keyframes` and SCSS's control flow belong in a stylesheet, not a sheet
|
|
9
|
+
- Setting attributes is recommended over toggling / mutating classes and ids, since the latter has a heavier impact on Quark's performance: a sheet that names any class wakes on every class change under its host (styling churn included)
|
|
10
|
+
- Rules do not revert when they stop matching — see [No reversion](./NO_REVERSION.md)
|
|
11
|
+
- `@view-transition` relies on `document.startViewTransition()`: one transition per document, no scoped transitions yet; `until` freezes the page while it waits (short waits only)
|
|
12
|
+
|
|
13
|
+
See the related section under [Limitations](/nucleus/docs/limitations) in the guides.
|
|
14
|
+
|
|
15
|
+
## Pitfalls
|
|
16
|
+
|
|
17
|
+
Do not render a template that re-matches the same rule — the loop guard cuts it after 50 nested paints, but the fix is the selector:
|
|
18
|
+
|
|
19
|
+
```quark
|
|
20
|
+
/* BAD — each new span matches again */
|
|
21
|
+
span {
|
|
22
|
+
content: template("#span-template");
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A sheet that registers after an element's connect-time events will not hear them (no replay): react to state attributes, or load the sheet first in its host.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Loop guard
|
|
2
|
+
|
|
3
|
+
Runaway write chains are cut, not prevented: one shared guard bounds every chain Quark, Neutron and `dom-observer` take part in.
|
|
4
|
+
|
|
5
|
+
## What it cuts
|
|
6
|
+
|
|
7
|
+
Two rules that flip each other's attributes, a rule and an element effect feeding each other, an event whose listener re-writes the attribute that fired it, or content that re-matches its own paint would otherwise run forever. The Nucleus Stack bounds every such chain with one shared guard, `LoopGuard` from `@excom/kit-utils` (Nucleus Kit users: `Neutron.DOM.LoopGuard`). Each write an engine makes carries the depth of the chain that caused it — Quark attributes, `content`, `$bindings`; Neutron attribute reflection and observed property assignments; `dom-observer` events — and the hop past the limit is **dropped**:
|
|
8
|
+
|
|
9
|
+
- default limit **50** dependent writes (a legitimate chain is under ten);
|
|
10
|
+
- the dropped write is logged once (`Loop guard: a chain of 51 dependent writes reached "data-x" on <p> — …`) and published to DevTools as `quark/error`;
|
|
11
|
+
- nothing else happens: no event fires, no attribute is wiped, the sheet is not disabled, and the document keeps the state it had before the drop — the cycle's participants stay wherever the chain left them, so treat a trip as a bug report, not a recovery;
|
|
12
|
+
- a new external write in a later task (user input, a timer, a fetch, app JS) starts a fresh chain at zero, so streams of updates never trip, however long.
|
|
13
|
+
|
|
14
|
+
## API
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { LoopGuard } from "@excom/kit-utils";
|
|
18
|
+
|
|
19
|
+
LoopGuard.limit; // 50
|
|
20
|
+
LoopGuard.configure({ limit: 100, log: (message) => myLogger.error(message) });
|
|
21
|
+
const off = LoopGuard.onTrip(({ kind, target, name, depth, limit }) => {
|
|
22
|
+
// kind: "depth" (a write chain) | "batch" (one Neutron handler re-ran > limit times)
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`configure()` is global and takes effect immediately. `onTrip` fires on every trip (the console line is deduplicated per name per task). Rules that gate on attributes they write for each other are named in a warning when the sheet builds, before anything runs.
|
|
27
|
+
|
|
28
|
+
## Not covered
|
|
29
|
+
|
|
30
|
+
Writes Quark and Neutron do not route — plain `setAttribute` / `innerHTML` in app JS — neither count nor get cut, and a declaration is never re-run by the attribute it wrote (`class:` excepted), so a rule cannot loop on its own gate (see [Reactivity](./REACTIVITY.md#md-loops)).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Allowed methods
|
|
2
|
+
|
|
3
|
+
Prototype methods an expression may call on a value through the dot accessor. Everything else is an error; wrap other logic in a `@use` module function.
|
|
4
|
+
|
|
5
|
+
## Reference
|
|
6
|
+
|
|
7
|
+
*Generated.*
|
|
8
|
+
|
|
9
|
+
<!-- generated:allowed-methods -->
|
|
10
|
+
| Method | On | Description |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| `.toLowerCase()` | string | Lower-cased copy. |
|
|
13
|
+
| `.toUpperCase()` | string | Upper-cased copy. |
|
|
14
|
+
| `.trim()` | string | Copy without leading / trailing whitespace. |
|
|
15
|
+
| `.split(separator, limit?)` | string | Array of substrings. |
|
|
16
|
+
| `.charAt(index)` | string | The character at `index`. |
|
|
17
|
+
| `.startsWith(search, position?)` | string | Boolean. |
|
|
18
|
+
| `.endsWith(search, length?)` | string | Boolean. |
|
|
19
|
+
| `.padStart(length, fill?)` | string | Left-padded copy. |
|
|
20
|
+
| `.padEnd(length, fill?)` | string | Right-padded copy. |
|
|
21
|
+
| `.replace(search, replacement)` | string | Copy with the first match replaced (string search only). |
|
|
22
|
+
| `.replaceAll(search, replacement)` | string | Copy with every match replaced (string search only). |
|
|
23
|
+
| `.toString()` | string, array, number, date | String form. |
|
|
24
|
+
| `.includes(search)` | string, array | Boolean. |
|
|
25
|
+
| `.slice(start?, end?)` | string, array | Sub-range copy. |
|
|
26
|
+
| `.indexOf(search)` | string, array | First position, or `-1`. |
|
|
27
|
+
| `.lastIndexOf(search)` | string, array | Last position, or `-1`. |
|
|
28
|
+
| `.concat(...values)` | string, array | Joined copy. |
|
|
29
|
+
| `.at(index)` | string, array | Element at `index`; negative counts from the end. |
|
|
30
|
+
| `.join(separator?)` | array | String of the items. |
|
|
31
|
+
| `.flat(depth?)` | array | Flattened copy. |
|
|
32
|
+
| `.toFixed(digits?)` | number | Fixed-point string. |
|
|
33
|
+
| `.toLocaleString(locale?, options?)` | number, date, array | Locale-formatted string. |
|
|
34
|
+
| `.toLocaleDateString(locale?, options?)` | date | Locale-formatted date. |
|
|
35
|
+
| `.toLocaleTimeString(locale?, options?)` | date | Locale-formatted time. |
|
|
36
|
+
| `.toISOString()` | date | ISO 8601 string. |
|
|
37
|
+
| `.getAttribute("name")` | element | Attribute value or `null`. Not observed. |
|
|
38
|
+
| `.hasAttribute("name")` | element | Boolean. Not observed. |
|
|
39
|
+
| `.matches("selector")` | element | Boolean. |
|
|
40
|
+
| `.closest("selector")` | element | Nearest ancestor-or-self matching the selector, or `null`. |
|
|
41
|
+
<!-- /generated -->
|
|
42
|
+
|
|
43
|
+
Own-property functions on a value (a `@use` namespace, a provided object) are always callable; see [Expressions](./EXPRESSIONS.md).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Built-in modules
|
|
2
|
+
|
|
3
|
+
Pure helpers for numbers, lists, maps, strings and dates, imported like JS modules — `@use "quark:math" as math;` — so nothing is global.
|
|
4
|
+
|
|
5
|
+
## Using a module
|
|
6
|
+
|
|
7
|
+
```quark
|
|
8
|
+
@use "quark:math" as math;
|
|
9
|
+
@use "quark:list" as *;
|
|
10
|
+
|
|
11
|
+
[data-progress] { --progress: math.percentage($done / $total); }
|
|
12
|
+
[bind-count] { content: count($todos, "done", true); }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`as name` namespaces the functions (`math.clamp(0, $x, 1)`, `math.$pi`); `as *` merges them into the bare scope. Every function is pure and null-tolerant: a missing collection reads as empty, a missing value passes through, and results are copies. Collection functions take a dot path (`"user.name"`) instead of a callback.
|
|
16
|
+
|
|
17
|
+
## Reference
|
|
18
|
+
|
|
19
|
+
*Generated.*
|
|
20
|
+
|
|
21
|
+
<!-- generated:builtin-modules -->
|
|
22
|
+
**`quark:math`** — Numbers. Arguments are coerced with `parseFloat`; the CSS argument order is kept.
|
|
23
|
+
|
|
24
|
+
| Name | Description |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `math.$pi` | π. |
|
|
27
|
+
| `math.$e` | Euler's number. |
|
|
28
|
+
| `min(...values)` | The smallest value (lists are flattened). |
|
|
29
|
+
| `max(...values)` | The largest value (lists are flattened). |
|
|
30
|
+
| `clamp(min, value, max)` | `value` limited to the range, in CSS argument order. |
|
|
31
|
+
| `round(value, digits?)` | Rounded to `digits` decimals (default 0). |
|
|
32
|
+
| `floor(value)` | Rounded down. |
|
|
33
|
+
| `ceil(value)` | Rounded up. |
|
|
34
|
+
| `abs(value)` | Absolute value. |
|
|
35
|
+
| `mod(value, divisor)` | Wrapping modulo: `mod(-1, 3)` is `2` (the `%` operator keeps the sign). `NaN` for a zero divisor. |
|
|
36
|
+
| `pow(base, exponent)` | `base` to the power `exponent`. |
|
|
37
|
+
| `sqrt(value)` | Square root. |
|
|
38
|
+
| `percentage(fraction)` | `"25%"` for `0.25` — a string, ready for an attribute or CSS variable. |
|
|
39
|
+
|
|
40
|
+
**`quark:list`** — Arrays (an object counts as the list of its values). `"path"` arguments are dot paths into each item; comparisons are loose (`==`), like `find()`.
|
|
41
|
+
|
|
42
|
+
| Name | Description |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `count(list, "path"?, value?)` | The number of items; with a path, the items whose value at it is non-empty; with a value too, the items equal to it. `0` for a missing list. |
|
|
45
|
+
| `find(list, "path", value)` | The first item whose value at the path equals `value`, else `undefined` (was a global built-in before 2026-09-13). |
|
|
46
|
+
| `filter(list, "path", value?)` | The items whose value at the path equals `value` — or is non-empty when `value` is omitted. |
|
|
47
|
+
| `reject(list, "path", value?)` | The complement of `filter`. |
|
|
48
|
+
| `pluck(list, "path")` | The value at the path of every item. |
|
|
49
|
+
| `sort-by(list, "path"?, "desc"?)` | A sorted copy: numbers numerically, everything else with a locale-aware, numeric-aware comparison; `null` last. `"desc"` reverses. |
|
|
50
|
+
| `sum(list, "path"?)` | The total of the items (or of their value at the path); non-numbers count as 0. |
|
|
51
|
+
| `range(end) / range(start, end, step?)` | `range(3)` → `[0, 1, 2]`; `range(1, 4)` → `[1, 2, 3]`; counts down when `start > end`. For skeleton rows and pagination. |
|
|
52
|
+
| `unique(list, "path"?)` | A copy without duplicates (by the value at the path when given), first occurrence kept. |
|
|
53
|
+
| `group-by(list, "path")` | A map from each distinct value at the path to the items carrying it. |
|
|
54
|
+
| `first(list)` | The first item, or `undefined`. |
|
|
55
|
+
| `last(list)` | The last item, or `undefined`. |
|
|
56
|
+
| `reverse(list)` | A reversed copy (was a global built-in before 2026-09-13). |
|
|
57
|
+
| `compact(list)` | A copy without `null`, `undefined`, `""`, empty lists and empty maps. |
|
|
58
|
+
|
|
59
|
+
**`quark:map`** — Plain objects (`(key: value)` literals, provisions, `dataset`-shaped data). Never mutates; returns copies.
|
|
60
|
+
|
|
61
|
+
| Name | Description |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `get(map, "path", fallback?)` | The value at the dot path, or `fallback` when it is missing. |
|
|
64
|
+
| `has-key(map, "path")` | Whether the dot path resolves to a value. |
|
|
65
|
+
| `keys(map)` | The keys, in insertion order. |
|
|
66
|
+
| `values(map)` | The values, in insertion order. |
|
|
67
|
+
| `entries(map)` | `[(key: …, value: …), …]` — iterate a map with `item.key` / `item.value` in the rows. |
|
|
68
|
+
| `merge(...maps)` | A shallow merge, later maps winning. |
|
|
69
|
+
| `pick(map, ...keys)` | A copy holding only the named keys. |
|
|
70
|
+
| `omit(map, ...keys)` | A copy without the named keys. |
|
|
71
|
+
|
|
72
|
+
**`quark:string`** — Text. Values are stringified first; `null` / `undefined` read as `""`.
|
|
73
|
+
|
|
74
|
+
| Name | Description |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `plural(count, forms, locale?)` | The form for `count` from a map keyed by `Intl.PluralRules` category (`one`, `other`, `few`, …); `#` in the form is replaced by the count: `plural($n, (one: "# item", other: "# items"))`. |
|
|
77
|
+
| `escape-html(value)` | `& < > " '` escaped, for text that goes through `dangerous-html()`. |
|
|
78
|
+
| `truncate(value, max, suffix?)` | Cut to `max` characters including the suffix (default `…`). |
|
|
79
|
+
| `capitalize(value)` | First character upper-cased. |
|
|
80
|
+
| `slugify(value)` | Lower-case ASCII with dashes: `"Héllo World!"` → `"hello-world"`. |
|
|
81
|
+
|
|
82
|
+
**`quark:date`** — Dates arrive as strings; these parse and format them. Every function accepts a `Date`, an ISO string or a timestamp and returns `null` / `""` for an unparseable value.
|
|
83
|
+
|
|
84
|
+
| Name | Description |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `parse(value)` | A `Date`, or `null` — for the allowed date methods (`toLocaleDateString()`, `toISOString()`). |
|
|
87
|
+
| `is-valid(value)` | Whether the value parses as a date. |
|
|
88
|
+
| `format(value, locale?, options?)` | `Intl.DateTimeFormat` output: `format($when, "en-GB", (dateStyle: "medium"))`. |
|
|
89
|
+
| `add(value, amount, unit?)` | A new date `amount` units later (`"days"` by default; seconds … weeks, months, years — months and years step the calendar). |
|
|
90
|
+
| `diff(later, earlier, unit?)` | Whole units between two dates (`"days"` by default), negative when `later` is earlier. |
|
|
91
|
+
|
|
92
|
+
**`quark:url`** — Query strings, via `URLSearchParams`.
|
|
93
|
+
|
|
94
|
+
| Name | Description |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `query(map)` | `"q=a+b&page=2"` from a map; `null` / `""` values are dropped, lists repeat the key. |
|
|
97
|
+
| `params(url)` | The query of a URL (or a bare query string) as a map; repeated keys become lists. |
|
|
98
|
+
| `encode(value)` | `encodeURIComponent`. |
|
|
99
|
+
|
|
100
|
+
**`quark:util`** — Small value helpers.
|
|
101
|
+
|
|
102
|
+
| Name | Description |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `coalesce(...values)` | The first value that is not `null` / `undefined` (`or` also skips `0`, `""` and `false`). |
|
|
105
|
+
| `is-empty(value)` | `true` for `null`, `undefined`, `""`, an empty list or an empty map. |
|
|
106
|
+
| `type-of(value)` | `"string"`, `"number"`, `"boolean"`, `"list"`, `"map"`, `"date"`, `"null"`, `"undefined"` or `"function"`. |
|
|
107
|
+
| `to-json(value, indent?)` | `JSON.stringify`, for `content` or a `data-*` attribute. |
|
|
108
|
+
| `from-json(text)` | `JSON.parse`, or `null` when the text is not JSON. |
|
|
109
|
+
<!-- /generated -->
|
|
110
|
+
|
|
111
|
+
Functions available without an import: [Built-in functions](./BUILTINS.md).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# No reversion
|
|
2
|
+
|
|
3
|
+
Quark rules do not undo their writes when they stop matching — the one deliberate divergence from CSS.
|
|
4
|
+
|
|
5
|
+
## Write the inverse rule
|
|
6
|
+
|
|
7
|
+
In CSS, a rule's declarations stop applying the moment its selector stops matching. **Quark rules do not revert** — this applies to every property kind: attributes, content, listeners, `$variables`, and CSS variables persist after the rule that set them stops matching. Write the counter-rule for every state you leave:
|
|
8
|
+
|
|
9
|
+
```quark
|
|
10
|
+
details[open] { --border-color: "red"; }
|
|
11
|
+
details:not([open]) { --border-color: "transparent"; }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The same applies to `:has()` facts (`:scope:has(…)` + `:scope:not(:has(…))`, see [Selectors](./SELECTORS.md)) and to any attribute derived from a condition.
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
Quark does not own the document: anything can write an attribute outside a sheet's knowledge, so pretending a rule can be applied and unapplied like CSS would be dishonest. Unmatch reversion may be introduced in a future version.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @on
|
|
2
|
+
|
|
3
|
+
`@on` wires listeners inside a rule: one or more event names, an options group that filters and configures the listener, and a block applied once per event.
|
|
4
|
+
|
|
5
|
+
```quark
|
|
6
|
+
form {
|
|
7
|
+
@on submit (prevent-default, handle: saveDraft($draft)) { is-submitted: ""; }
|
|
8
|
+
@on input, change (debounce: 300) { data-draft: event.target.value; }
|
|
9
|
+
@on keydown (key: "Escape", host: window) { is-open: none; }
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The prelude says *when*: which events, which filters. The block says *what*: writes, `@dispatch` / `@command`, `@delay`. JS callouts are the `handle:` option. There is no handler list after the event names any more (`@on click go;` is a parse error that points at `handle:`).
|
|
14
|
+
|
|
15
|
+
## Events
|
|
16
|
+
|
|
17
|
+
`@on click`, `@on super-form-success`, `@on "my:event"` — a bare name or a string. A comma list shares one listener and one block: `@on input, change { … }`. `event.type` tells them apart inside the block.
|
|
18
|
+
|
|
19
|
+
## Blocks
|
|
20
|
+
|
|
21
|
+
`@on <events> [(options)] { … }` turns the event into a **one-shot transaction**: the block is an ordinary rule body, evaluated when the event fires instead of when the rule matches. Declarations write the matched element; nested rules write its matching descendants; `event` is the DOM event; `@dispatch` / `@command` statements fire after the writes are queued. It is how typed input, clicks and element events become State without JS:
|
|
22
|
+
|
|
23
|
+
```quark
|
|
24
|
+
:scope {
|
|
25
|
+
$count: +attr("data-count");
|
|
26
|
+
[bind-count] { content: $count; }
|
|
27
|
+
@on counter-increment { data-count: $count + 1; }
|
|
28
|
+
@on input {
|
|
29
|
+
data-draft: event.target.value;
|
|
30
|
+
#preview { content: event.target.value or preserve; }
|
|
31
|
+
}
|
|
32
|
+
@on submit (prevent-default) { is-submitted: ""; }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A nested rule whose selector starts with a sibling combinator writes the element's siblings instead of its descendants — `:scope { button { @on click { + provider-fetch { @command --fetch; } } } }` invokes `--fetch` on the `provider-fetch` right after the button.
|
|
37
|
+
|
|
38
|
+
Writes are batched like any rule's; `$variables` set in a block persist on the element. `@on` inside a block is not supported.
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
|
|
42
|
+
The **options group** after the events is a map: `name: value` entries and bare flags, which mean `true`. With options the block is optional — `@on submit (prevent-default);` is a complete statement. These are the same filters `<event-handler>` offers, in the sheet:
|
|
43
|
+
|
|
44
|
+
```quark
|
|
45
|
+
ul {
|
|
46
|
+
@on click (target: "li[data-id]") { data-selected: target.getAttribute("data-id"); }
|
|
47
|
+
@on keydown (key: "Escape", host: window) { is-open: none; }
|
|
48
|
+
@on input (debounce: 300) { data-query: event.target.value; }
|
|
49
|
+
@on scroll (throttle: 100, passive, handle: trackScroll);
|
|
50
|
+
@on click (self, once, prevent-default);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Option | Effect |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `target: "<selector>"` | Delegation: fires only when the event target is inside a descendant matching the selector; that element is `target` in the block and in the other options (`event.target` otherwise). With `host:` the selector is matched document-wide. |
|
|
57
|
+
| `self` | Fires only when the event target is the matched element itself. |
|
|
58
|
+
| `key: "Escape"` / `"Shift+K"` | Keyboard chord; space-separated tokens are alternatives (`"Escape Enter"`). Listed modifiers (`shift`, `alt`, `ctrl`, `meta` / `cmd`) must be held. |
|
|
59
|
+
| `prevent-default` / `stop-propagation` / `stop-immediate-propagation` | Act on the event as soon as it passes the filters, before any timing. |
|
|
60
|
+
| `debounce: <ms>` / `throttle: <ms>` | Wait for a pause / run at most once per window (leading edge). Exclusive. |
|
|
61
|
+
| `handle: fn` | JS: a function, a call returning one, or a list `(a, b)` — each called with the event, `this` being the element, before the block. `prevent-default` and `stop-propagation` are also functions, for lists. |
|
|
62
|
+
| `once` | Detach after the first event that passes the filters. |
|
|
63
|
+
| `passive` / `capture` | Native `addEventListener` options. |
|
|
64
|
+
| `host: window` / `host: document` | Register on the window / document while the element is in the document; the listener lets go on the first event after the element is removed. |
|
|
65
|
+
|
|
66
|
+
### When values are evaluated
|
|
67
|
+
|
|
68
|
+
`target`, `key`, `debounce`, `throttle` and `handle` are expressions evaluated **when the event fires**, in the block's scope: `event`, `target`, `element` and the element's current `$bindings` are all in reach.
|
|
69
|
+
|
|
70
|
+
```quark
|
|
71
|
+
#list {
|
|
72
|
+
$row: attr("data-row-selector");
|
|
73
|
+
@on click (target: $row, handle: pick(target.getAttribute("data-id"), event)) { … }
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
So nothing about a listener is reactive — a changed `$row` is read by the next click, and no re-run ever re-registers the DOM listener. The flags, `once` and `host` configure the registration and are read once per match; they must be bare words.
|
|
78
|
+
|
|
79
|
+
Two `@on`s for one event may coexist when their options differ (`(key: "Escape")` and `(key: "Enter")`).
|
|
80
|
+
|
|
81
|
+
## Outgoing events
|
|
82
|
+
|
|
83
|
+
`@dispatch` and `@command` inside the block send events and commands from it — see [`@dispatch` / `@command`](./DISPATCH.md).
|
|
84
|
+
|
|
85
|
+
## No `@off`
|
|
86
|
+
|
|
87
|
+
There is no `@off`: a listener you would remove is a listener that should not fire — gate it with options or with event data inside its block. Listeners persist like any other write (see [No reversion](./NO_REVERSION.md)).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Reactivity
|
|
2
|
+
|
|
3
|
+
A rule runs when an element matches it and re-runs when something its selector or its values depend on changes — nothing more, nothing less.
|
|
4
|
+
|
|
5
|
+
## When a rule re-runs
|
|
6
|
+
|
|
7
|
+
- an attribute, class (`.x`) or id (`#x`) named in its selector changing on the matched element, on an ancestor or earlier sibling on the selector's path, or on a descendant named in `:has()` (a class change counts only when it adds or removes a class the sheet names, unless a rule needs the whole value: `[class~="x"]`, `attr("class")`);
|
|
8
|
+
- a literal `attr("x")` in one of its values, when `x` changes on the matched element;
|
|
9
|
+
- a `$binding` one of its values reads, when that binding changes on an ancestor-or-self owner (the nearest owner wins, so a farther change is ignored);
|
|
10
|
+
- a literal `prop("x")` in one of its values, when JS assigns `element.x`;
|
|
11
|
+
- elements being inserted anywhere under the host, whether by Quark, an element, or app JS (a `childList` MutationObserver): rules matching the new elements run, `content` rules below the insertion point re-run, and `:has()` / `:empty` candidates above it are re-checked;
|
|
12
|
+
- elements being removed, only while some rule's match depends on children or sibling position (`:has()`, `:empty`, `:nth-child()`, `a + b`): the same re-runs as an insertion at that parent. Text-only changes are never observed.
|
|
13
|
+
|
|
14
|
+
## Not observed
|
|
15
|
+
|
|
16
|
+
Interaction and validity pseudo-classes (`:hover`, `:focus`, `:checked`, `:invalid`, … — warned at build), pseudo-elements, `attr($dynamic)` / `prop($dynamic)`, `closest()` and DOM method reads (`getAttribute`, `matches`), in-place mutation of an object a `prop()` or `$binding` holds, and browser-driven native state that does not go through a JS setter (typing into an `<input>`, `<details>` toggling). Select on reflected attributes or listen to events for those. The reverse direction is covered: `value` / `checked` / `selected` / textarea `content` writes also set the live property on native form controls (see [Attributes](./ATTRIBUTES.md)).
|
|
17
|
+
|
|
18
|
+
## Loops
|
|
19
|
+
|
|
20
|
+
A declaration is never re-run by the attribute it wrote (the mutation that woke the rule is excluded from that pass), so a rule cannot loop on its own gate. `class:` is the exception: classes are separate facts, so `.is-done { class: (is-struck: true); }` applies when `is-done` arrives. Setting attributes is recommended over toggling / mutating classes and ids, since the latter has a heavier impact on Quark's performance. Cycles through two or more attributes or classes, `$bindings` across sheets, content that re-matches its own paint, element effects or events are cut by the shared [loop guard](./LOOP_GUARD.md): every write carries the depth of the chain that caused it — a deferred paint keeps the depth of the run that scheduled it; an observer record, an `attributeChangedCallback` or a property-change event inherits the depth stamped on the address it reacts to — and the hop past `LoopGuard.limit` (50) is dropped, logged once and published as `quark/error`. Chains restart at zero on every external write in a later task, so a stream of provider updates never trips. Rules that gate on attributes they write for each other are warned about when the sheet builds (`Quark: rules gate on attributes they write for each other — …`).
|
|
21
|
+
|
|
22
|
+
## Timing
|
|
23
|
+
|
|
24
|
+
Listeners and `$variables` apply synchronously; attributes, CSS variables and content are painted in a batch one task later, and the rules that depend on those writes run in the next cycle. Writes more than a tick apart are separate facts.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# quark
|
|
2
|
+
|
|
3
|
+
CSS-like orchestration for your HTML — bind attributes, render lists, and wire events without a component tree.
|
|
4
|
+
|
|
5
|
+
Quark is a derivative of CSS with CSS-compatible syntax (a CSS parser can tokenize Quark). The language is familiar. Rule properties are HTML attributes. The utilities (at-rules, util functions) are new.
|
|
6
|
+
|
|
7
|
+
Prefer `<quark-sheet>` for apps; use the `Quark` class when you need a programmatic host (tests, tooling).
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **CSS-like sheets** Selectors + nested rules that mutate the live DOM
|
|
12
|
+
- **`$variables`** Scoped values that nest and resolve in expressions
|
|
13
|
+
- **JS writes** `element.quark.setProperty()` writes a `$variable` from JavaScript, on any element
|
|
14
|
+
- **CSS variables** Write `--custom-props` from state; style via `var()`
|
|
15
|
+
- **Content rendering** `content`, `template()`, `iterate()`, `dangerous-html()`
|
|
16
|
+
- **Element properties** `prop("provision")` reads Neutron provisions / any JS property, re-running on assignment
|
|
17
|
+
- **Events** `@on` at-rules with delegation, key, timing and host options, plus `prevent-default` / `stop-propagation`
|
|
18
|
+
- **View transitions** `@view-transition` commits a block's writes inside `document.startViewTransition()`, so CSS animates list changes, removals included
|
|
19
|
+
- **Delayed writes** `@delay 2000 { … }` applies a block after a pause — flashes, toasts, undo windows — dropped if the rule stopped matching
|
|
20
|
+
- **Diagnostics** `@warn` / `@debug` / `@error` report from a rule; the selector is the condition
|
|
21
|
+
- **Built-in modules** `@use "quark:math"`, `quark:list`, `quark:map`, `quark:string`, `quark:date`, `quark:url`, `quark:util` — pure helpers, imported like JS modules
|
|
22
|
+
- **Attribute helpers** `dataset`, `ariaset`, `class`, `none` to clear
|
|
23
|
+
- **JS modules** Call app helpers from expressions via `@use "/url"`
|
|
24
|
+
- **Scoped host** Sheet + targets share a parent; updates follow DOM mutations
|
|
25
|
+
- **DevTools** `Quark.attachDevtools()` reports rule applications and `$variables` to the Nucleus DevTools extension
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
<include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
App authors almost always load Quark through `<quark-sheet>`:
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<section>
|
|
37
|
+
<quark-sheet>
|
|
38
|
+
details[open] [bind-status] {
|
|
39
|
+
content: "Open";
|
|
40
|
+
}
|
|
41
|
+
details:not([open]) [bind-status] {
|
|
42
|
+
content: "Closed";
|
|
43
|
+
}
|
|
44
|
+
</quark-sheet>
|
|
45
|
+
<details>
|
|
46
|
+
<summary>Panel</summary>
|
|
47
|
+
<span bind-status></span>
|
|
48
|
+
</details>
|
|
49
|
+
</section>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Programmatic API (tests / custom hosts):
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { Quark } from "@excom/quark";
|
|
56
|
+
|
|
57
|
+
const quark = new Quark({
|
|
58
|
+
src: `span { content: "four times two equals #{twice(4)}"; }`,
|
|
59
|
+
modules: { dfault: { twice: (n) => n * 2 } },
|
|
60
|
+
});
|
|
61
|
+
quark.register({ sheetElement }); // host = sheetElement.parentElement
|
|
62
|
+
// …
|
|
63
|
+
quark.unregister();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Documentation
|
|
67
|
+
|
|
68
|
+
Syntax
|
|
69
|
+
|
|
70
|
+
- [Sheets & scoping](./SHEETS.md) — `<quark-sheet>`, `@scope`, `is-global`, what runs
|
|
71
|
+
- [Syntax](./SYNTAX.md) — rules, declarations, literals, operators, `if()`
|
|
72
|
+
|
|
73
|
+
Selectors
|
|
74
|
+
|
|
75
|
+
- [Selectors](./SELECTORS.md) — combinators and pseudo-classes, what is observed
|
|
76
|
+
- [Reactivity](./REACTIVITY.md) — when a rule re-runs, loops, timing
|
|
77
|
+
- [No reversion](./NO_REVERSION.md) — write the inverse rule
|
|
78
|
+
|
|
79
|
+
Declarations
|
|
80
|
+
|
|
81
|
+
- [Declaration kinds](./DECLARATIONS.md) — what a key does
|
|
82
|
+
- [Attributes](./ATTRIBUTES.md) — attributes, `class` / `dataset` / `ariaset`, form controls
|
|
83
|
+
- [Content](./CONTENT.md) — `content`, `template()`, `iterate()`, `dangerous-html()`
|
|
84
|
+
- [CSS variables](./CSS_VARIABLES.md) — `--custom-props`
|
|
85
|
+
|
|
86
|
+
Values & expressions
|
|
87
|
+
|
|
88
|
+
- [Variables](./VARIABLES.md) — `$variables`, cascade, `unset`, raising state
|
|
89
|
+
- [Writing from JS](./JS_WRITES.md) — `element.quark.setProperty()`
|
|
90
|
+
- [Values & keywords](./VALUES.md) — `none` / `preserve` / `unset`, wipes and no-ops
|
|
91
|
+
- [Expressions](./EXPRESSIONS.md) — name resolution, operators, `if()`, lists and maps
|
|
92
|
+
- [Built-in functions](./BUILTINS.md) — `attr()`, `prop()`, `iterate()`, `event`, …
|
|
93
|
+
- [Allowed methods](./METHODS.md) — `.toFixed()`, `.join()`, …
|
|
94
|
+
- [Built-in modules](./MODULES.md) — `@use "quark:math"`, `quark:list`, `quark:map`, `quark:string`, `quark:date`, `quark:url`, `quark:util`
|
|
95
|
+
- [Element properties](./ELEMENT_PROPERTIES.md) — `prop("provision")`, `element`
|
|
96
|
+
|
|
97
|
+
At-rules
|
|
98
|
+
|
|
99
|
+
- [At-rules](./AT_RULES.md) — the ones that run
|
|
100
|
+
- [`@use`](./USE.md) — JS modules
|
|
101
|
+
- [`@on`](./ON.md) — events, blocks, options
|
|
102
|
+
- [`@dispatch` / `@command`](./DISPATCH.md) — outgoing events and commands
|
|
103
|
+
- [`@view-transition`](./VIEW_TRANSITION.md) — animated writes
|
|
104
|
+
- [`@delay`](./DELAY.md) — deferred writes
|
|
105
|
+
- [`@warn` / `@debug` / `@error`](./DIAGNOSTICS.md) — diagnostics from a rule
|
|
106
|
+
|
|
107
|
+
Runtime
|
|
108
|
+
|
|
109
|
+
- [JS API](./JS_API.md) — `Quark`, `whenSettled()`, DevTools
|
|
110
|
+
- [Loop guard](./LOOP_GUARD.md) — runaway chains are cut
|
|
111
|
+
- [Limitations](./LIMITATIONS.md) — beta limits and pitfalls
|
|
112
|
+
|
|
113
|
+
The grammar (EBNF, precedence, AST) is the `quark-parser` package's *Language reference*.
|
|
114
|
+
|
|
115
|
+
### Examples
|
|
116
|
+
|
|
117
|
+
#### React to element state
|
|
118
|
+
|
|
119
|
+
Native element state drives content — no JS, no listeners:
|
|
120
|
+
|
|
121
|
+
<include-content data-demo="toggle-content"></include-content>
|
|
122
|
+
|
|
123
|
+
#### List from a provider
|
|
124
|
+
|
|
125
|
+
`prop("provision")` pulls the fetch payload; `iterate()` renders a row per item and re-renders on every provision:
|
|
126
|
+
|
|
127
|
+
<include-content data-demo="provider-list"></include-content>
|
|
128
|
+
|
|
129
|
+
#### Call a module helper
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<quark-sheet>
|
|
133
|
+
@use "/helpers.js" as *;
|
|
134
|
+
|
|
135
|
+
#out { content: formatPrice($amount); }
|
|
136
|
+
</quark-sheet>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Count clicks from JS
|
|
140
|
+
|
|
141
|
+
The sheet hands the owner element to a JS helper; each click calls `owner.quark.setProperty("$count", …)` and every rule reading `$count` below the owner re-runs. See [Writing from JS](./JS_WRITES.md).
|
|
142
|
+
|
|
143
|
+
<include-content data-demo="js-api"></include-content>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Selectors
|
|
2
|
+
|
|
3
|
+
Quark rides the native selector engine: whatever `querySelectorAll` matches, a rule matches. What Quark adds is observation — knowing which changes can flip a match and where the subjects are afterwards.
|
|
4
|
+
|
|
5
|
+
## What is observed
|
|
6
|
+
|
|
7
|
+
Attributes, classes and ids named anywhere in a selector are observed on the element they sit on: the matched element, an ancestor or earlier sibling on the selector's path, or a descendant named in `:has()`. Structure is observed too: elements inserted or removed under the host re-check the rules whose match depends on children or sibling position. *Generated.*
|
|
8
|
+
|
|
9
|
+
<!-- generated:selectors -->
|
|
10
|
+
**Combinators**
|
|
11
|
+
|
|
12
|
+
| Combinator | Observed |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `a b` | Descendant. An attribute change on `a` re-runs the rule for the matching `b`s below it. |
|
|
15
|
+
| `a > b` | Child. Same observation as the descendant combinator. |
|
|
16
|
+
| `a + b` | Next sibling. An attribute change on `a` re-runs the rule from the parent; elements inserted or removed under that parent re-run it too. |
|
|
17
|
+
| `a ~ b` | Subsequent siblings. Same observation as `+`. |
|
|
18
|
+
|
|
19
|
+
**Logical**
|
|
20
|
+
|
|
21
|
+
| Pseudo-class | Observed |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `:is(…)` | Attributes anywhere in the argument list are observed like the compound's own. A complex argument (`:is(section[x] li)`) re-runs the rule from the changed element down. |
|
|
24
|
+
| `:where(…)` | Same as `:is()` (Quark has no specificity). |
|
|
25
|
+
| `:not(…)` | Same observation as `:is()`. |
|
|
26
|
+
|
|
27
|
+
**Relational**
|
|
28
|
+
|
|
29
|
+
| Pseudo-class | Observed |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `:has(…)` | Attributes named in the argument are observed on descendants, and elements inserted or removed below a candidate re-check it: the rule re-runs for every matching ancestor of the change. Sibling-relative arguments (`:has(+ …)`, `:has(~ …)`) and a `:has()` nested in a complex `:is()` / `:not()` argument re-run the whole rule from the host instead. Without rule reversion, pair it with the inverse `:not(:has(…))` rule. |
|
|
32
|
+
|
|
33
|
+
**Structural (sibling position, children)**
|
|
34
|
+
|
|
35
|
+
| Pseudo-class | Observed |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `:first-child` | Sibling position: re-runs when elements are inserted or removed under the parent. |
|
|
38
|
+
| `:last-child` | Same as `:first-child`. |
|
|
39
|
+
| `:only-child` | Same as `:first-child`. |
|
|
40
|
+
| `:nth-child(An+B [of S])` | Same as `:first-child`. With `of S`, attributes in `S` are observed on the siblings and each change re-runs the rule from the parent. |
|
|
41
|
+
| `:nth-last-child(An+B [of S])` | Same as `:nth-child()`. |
|
|
42
|
+
| `:first-of-type` | Same as `:first-child`. |
|
|
43
|
+
| `:last-of-type` | Same as `:first-child`. |
|
|
44
|
+
| `:only-of-type` | Same as `:first-child`. |
|
|
45
|
+
| `:nth-of-type(An+B)` | Same as `:first-child`. |
|
|
46
|
+
| `:nth-last-of-type(An+B)` | Same as `:first-child`. |
|
|
47
|
+
| `:empty` | Re-checked when elements are inserted or removed below the element. Text-only changes are not observed. |
|
|
48
|
+
|
|
49
|
+
**Attribute-backed**
|
|
50
|
+
|
|
51
|
+
| Pseudo-class | Observed |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `:disabled` | Observes `disabled` on the element and on ancestors (a disabled `<fieldset>`). |
|
|
54
|
+
| `:enabled` | Same as `:disabled`. |
|
|
55
|
+
| `:required` | Observes `required`. |
|
|
56
|
+
| `:optional` | Same as `:required`. |
|
|
57
|
+
| `:read-only` | Observes `readonly`, `disabled` and `contenteditable` on the element and its ancestors. |
|
|
58
|
+
| `:read-write` | Same as `:read-only`. |
|
|
59
|
+
| `:any-link` | Observes `href`. |
|
|
60
|
+
| `:lang(…)` | Observes `lang` on the element and its ancestors. |
|
|
61
|
+
| `:open` | Observes the `open` attribute (`<details>`, `<dialog>`). A `<select>` / `<input>` picker opening is not observed. |
|
|
62
|
+
|
|
63
|
+
**Static**
|
|
64
|
+
|
|
65
|
+
| Pseudo-class | Observed |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `:scope` | The host; never changes. |
|
|
68
|
+
| `:root` | The document element; never changes. |
|
|
69
|
+
|
|
70
|
+
**Not observed** — `:hover`, `:focus`, `:focus-within`, `:focus-visible`, `:active`, `:visited`, `:link`, `:target`, `:checked`, `:indeterminate`, `:default`, `:valid`, `:invalid`, `:in-range`, `:out-of-range`, `:placeholder-shown`, `:popover-open`, `:modal`, `:fullscreen`, `:defined`, `:dir(…)`: Interaction or browser state with no attribute behind it: matches on the first run only (warned at build). Select on reflected attributes instead. Any pseudo-class not listed above is treated the same way.
|
|
71
|
+
<!-- /generated -->
|
|
72
|
+
|
|
73
|
+
## `:has()`
|
|
74
|
+
|
|
75
|
+
`:has()` costs a native subtree scan per candidate on every fan-out that includes the rule, plus one ancestor walk per observed change — keep its arguments shallow and off rules that match many rows. Without rule reversion a `:has()` rule needs its inverse (`:not(:has(…))`) like any other state:
|
|
76
|
+
|
|
77
|
+
```quark
|
|
78
|
+
/* an aggregate over the rows becomes a fact on the host */
|
|
79
|
+
:scope:has(li[data-is-selected]) { data-has-selection: ""; }
|
|
80
|
+
:scope:not(:has(li[data-is-selected])) { data-has-selection: none; }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Classes and ids
|
|
84
|
+
|
|
85
|
+
Class (`.x`) and id (`#x`) selectors are observed like attributes (a class change counts only when it adds or removes a class the sheet names). Setting attributes is still recommended over toggling / mutating classes and ids, since the latter has a heavier impact on Quark's performance: a sheet that names any class wakes on every class change under its host (styling churn included).
|
|
86
|
+
|
|
87
|
+
When a rule re-runs, and what stays unobserved: [Reactivity](./REACTIVITY.md).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Sheets & scoping
|
|
2
|
+
|
|
3
|
+
A sheet lives next to the elements it orchestrates; its rules reach the host's subtree and nothing else, exactly like an inline `<style>` with `@scope`.
|
|
4
|
+
|
|
5
|
+
## Loading a sheet
|
|
6
|
+
|
|
7
|
+
App authors almost always load Quark through `<quark-sheet>`:
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<section>
|
|
11
|
+
<quark-sheet>
|
|
12
|
+
details[open] [bind-status] {
|
|
13
|
+
content: "Open";
|
|
14
|
+
}
|
|
15
|
+
details:not([open]) [bind-status] {
|
|
16
|
+
content: "Closed";
|
|
17
|
+
}
|
|
18
|
+
</quark-sheet>
|
|
19
|
+
<details>
|
|
20
|
+
<summary>Panel</summary>
|
|
21
|
+
<span bind-status></span>
|
|
22
|
+
</details>
|
|
23
|
+
</section>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Sheets can also be fetched (`<quark-sheet src-url="/views/app.quark">`) or constructed programmatically — see [JS API](./JS_API.md).
|
|
27
|
+
|
|
28
|
+
## Scoping
|
|
29
|
+
|
|
30
|
+
By default (`<quark-sheet>` without `is-global`, or the `isScoped` constructor option), sheets are implicitly wrapped in `@scope { … }`, anchored at the host (the sheet's parent element) — the same model and semantics as CSS inline-`<style>` scoping. Global sheets (`<quark-sheet is-global>`) skip the wrap: top-level rules run in the root context — e.g. to read a provider that is an ancestor of the host — and rules the author nests in an explicit `@scope { }` block stay host-scoped. Every part of a selector matches strict descendants of the host: an ancestor compound (e.g. the `provider-fetch[is-success]` in `provider-fetch[is-success] h4`) is only satisfied by an element *inside* the host, never by the host itself or anything above it. The host is reachable only via the explicit `:scope` selector:
|
|
31
|
+
|
|
32
|
+
```quark
|
|
33
|
+
:scope { data-app: "ready"; } /* mutate the host itself */
|
|
34
|
+
:scope[data-mode="on"] [bind-x] { … } /* react to host state */
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Registered hosts carry a generated `q-scope="<id>"` attribute — Quark's scoping anchor. Treat it as internal: don't set, copy, or select on it.
|
|
38
|
+
|
|
39
|
+
## What runs
|
|
40
|
+
|
|
41
|
+
A sheet is minified (comments stripped, whitespace collapsed), optionally wrapped in `@scope { … }` (see above), and parsed. The runtime then executes:
|
|
42
|
+
|
|
43
|
+
- **Rules**, nested to any depth. A nested selector is joined to its parent as a descendant, or spliced over each `&`.
|
|
44
|
+
- **Declarations inside rules**, by key shape (see [Declaration kinds](./DECLARATIONS.md)).
|
|
45
|
+
- **`@use "url" [as name | as *]`** anywhere in the sheet. The namespace defaults to the URL's last path segment without its extension (`/api-client.js` → `api-client`); `as *` merges exports into the bare scope, last import winning on clashes. A `with (…)` clause is a parse error.
|
|
46
|
+
- **`@scope { … }`**, which takes no prelude: rules inside stay anchored to the host in a global sheet.
|
|
47
|
+
- **`@on <event> [(options)] …;`** inside rules: listeners (see [`@on`](./ON.md)).
|
|
48
|
+
- **`@view-transition [(options)] { … }`** inside rules, around rules or inside `@on` blocks: the writes inside it commit inside a view transition (see [`@view-transition`](./VIEW_TRANSITION.md)).
|
|
49
|
+
- **`@delay <ms> { … }`** inside rules and blocks: the block applies once, after the pause, if the rule still matches (see [`@delay`](./DELAY.md)).
|
|
50
|
+
- **`@warn` / `@debug` / `@error <expression>;`** inside rules and blocks: report on the matched element (see [Diagnostics](./DIAGNOSTICS.md)).
|
|
51
|
+
|
|
52
|
+
That list is the language: any other at-rule, a nested property block, a `%placeholder` selector, `#{…}` outside a string, and `!important` / `!default` / `!global` are parse errors, and a sheet that fails to parse does not run (`<quark-sheet>` sets `is-error`). Top-level declarations parse but have no element to write.
|
|
53
|
+
|
|
54
|
+
```quark
|
|
55
|
+
@use "/helpers.js" as *;
|
|
56
|
+
main {
|
|
57
|
+
$items: prop("provision").body;
|
|
58
|
+
ul {
|
|
59
|
+
content: iterate($items);
|
|
60
|
+
li { data-id: item.id; &[data-id="0"] { is-first: ""; } }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Load order
|
|
66
|
+
|
|
67
|
+
Imports (`@use`) and `src-url` sheets resolve before the first rule run, so a sheet that registers late misses events fired meanwhile — react to state attributes (`is-*`) rather than one-shot events, and put the sheet first in its host when it must hear boot-time events.
|