reviewzy 0.2.0 → 0.4.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.
@@ -0,0 +1,79 @@
1
+ // cursor-rules.js - pure, DOM-free, dynamic-imported by the ring at boot.
2
+ const UNUSABLE = /::|&/; // a pseudo-element is a box closest() can never return
3
+ const STATE_PSEUDO = /:(?:hover|active|focus|focus-visible|focus-within|visited|link|target)\b/g;
4
+
5
+ // clickable with no cursor declaration anywhere: the browser draws the pointer itself.
6
+ // No `label`: a field label is not a target, and a label that WRAPS its control takes its
7
+ // pointer from the stylesheet sweep instead, which is where the difference belongs.
8
+ export const SEMANTIC_POINTER = [
9
+ 'a[href]', 'area[href]', 'button', 'summary', 'select',
10
+ 'input[type="checkbox"]', 'input[type="radio"]', 'input[type="range"]',
11
+ 'input[type="color"]', 'input[type="file"]', 'input[type="submit"]',
12
+ 'input[type="reset"]', 'input[type="button"]',
13
+ '[role="button"]', '[role="link"]', '[role="tab"]',
14
+ '[role="menuitem"]', '[role="option"]', '[role="switch"]',
15
+ ].join(', ');
16
+ export const SEMANTIC_BLOCKED = ':disabled, [aria-disabled="true"]';
17
+
18
+ // split on the commas separating selectors, not the ones inside :is() or an attribute value
19
+ export function splitSelectorList(text) {
20
+ const parts = [];
21
+ let depth = 0, quote = '', start = 0;
22
+ for (let i = 0; i < text.length; i++) {
23
+ const c = text[i];
24
+ if (quote) { if (c === quote && text[i - 1] !== '\\') quote = ''; continue; }
25
+ if (c === '"' || c === "'") quote = c;
26
+ else if (c === '(' || c === '[') depth++;
27
+ else if (c === ')' || c === ']') depth--;
28
+ else if (c === ',' && depth === 0) { parts.push(text.slice(start, i)); start = i + 1; }
29
+ }
30
+ parts.push(text.slice(start));
31
+ return parts.map(s => s.trim()).filter(Boolean);
32
+ }
33
+
34
+ /* One walk for both collectors: every rule of every sheet, in document order. `classify`
35
+ reads a rule and returns its verdict, or a falsy value to skip it; `take` gets each
36
+ usable selector that rule names, with that verdict. A state pseudo is stripped before
37
+ `take` sees it, so `.x:hover` names `.x` and folds onto whatever `.x` already said. */
38
+ const sweep = (sheets, classify, take) => {
39
+ const visit = rules => {
40
+ for (const rule of rules || []) {
41
+ const verdict = rule.selectorText && rule.style ? classify(rule) : null;
42
+ if (verdict) for (const part of splitSelectorList(rule.selectorText)) {
43
+ if (UNUSABLE.test(part)) continue;
44
+ const clean = part.replace(STATE_PSEUDO, '').trim();
45
+ if (clean) take(clean, verdict);
46
+ }
47
+ if (rule.cssRules) visit(rule.cssRules); // @media, @supports, nesting
48
+ }
49
+ };
50
+ for (const sheet of sheets || []) {
51
+ try { visit(sheet.cssRules); } catch { /* cross-origin sheet, nothing worth a broken boot */ }
52
+ }
53
+ };
54
+
55
+ /* Every selector any rule gives one of `values` for `prop`, de-duplicated. It NARROWS a
56
+ walk, never decides one: the computed style still has the last word, so a selector
57
+ whose rule lives under a media query the viewport does not match costs one match and
58
+ nothing else. The alternative, reading `getComputedStyle` for every element in the
59
+ document, forces rendering inside any `content-visibility` subtree it crosses, which
60
+ Chrome reports and this system's collapsed rows all carry. */
61
+ export function collectPropSelectors(sheets, prop, values) {
62
+ const found = new Set();
63
+ sweep(sheets, rule => values.includes(rule.style[prop]), clean => found.add(clean));
64
+ return [...found];
65
+ }
66
+
67
+ // `inert` is the `cursor: default` set: it lets a rule that switches a clickable class back off
68
+ // (a current breadcrumb, a readonly field) outrank the pointer entry it was written to override.
69
+ export function collectCursorSelectors(sheets) {
70
+ const buckets = { pointer: new Set(), inert: new Set(), blocked: new Set() };
71
+ const bucketFor = c => c === 'pointer' ? buckets.pointer
72
+ : c === 'default' ? buckets.inert
73
+ : c === 'not-allowed' ? buckets.blocked : null;
74
+ sweep(sheets, rule => bucketFor(rule.style.cursor), (clean, bucket) => {
75
+ for (const b of Object.values(buckets)) b.delete(clean); // document order stands in for the cascade
76
+ bucket.add(clean);
77
+ });
78
+ return { pointer: [...buckets.pointer], inert: [...buckets.inert], blocked: [...buckets.blocked] };
79
+ }
@@ -0,0 +1,134 @@
1
+ /* ============================================================
2
+ the ambient layer, cursor v3
3
+ The jelly ring with its aim dot, lit up with speed; its tube
4
+ trail in the comet blue, leaving the mark 4px narrower than it is across the
5
+ motion and stopping 2px short of its keyline; the parting dot grid, its droplets at
6
+ speed and its click ripple plus splash. Everything the cursor
7
+ draws lives on one canvas inside #cursor, so every stroke keeps its
8
+ declared width whatever the ring's shape does. Load after components.css;
9
+ cursor.js boots it. Rules and mechanisms: cursor.md.
10
+ Contrast, from the repo root, `node tools/contrast.mjs "<fg>" "<bg>"`, on
11
+ --bg / --surface-1 / --surface-2 / --surface-3 / --well / --sky-peak:
12
+ dark ink oklch(0.76 0.110 238) 7.49 / 6.48 / 5.48 / 4.48 / 8.31 / 6.33
13
+ dark lit ink oklch(0.90 0.080 238) 11.73 / 10.15 / 8.59 / 7.01 / 13.02 / 9.92
14
+ dark ripple oklch(0.82 0.115 238) the trail core's row
15
+ dark keyline --bg on the --accent fill 7.49, on --accent-press 6.02
16
+ dark refuse oklch(0.72 0.140 15) 5.98 --bg, 5.18 --surface-1
17
+ dark trail core oklch(0.82 0.115 238) 9.15 / 7.91 / 6.70 / 5.47 / 10.15 / 7.73
18
+ dark trail edge --accent, the ink's row
19
+ dark spray oklch(0.88 0.070 238) 11.13 / 9.62 / 8.14 / 6.65 / 12.34 / 9.40
20
+ dark spray hot oklch(0.80 0.130 39) 7.93 / 6.86 / 5.81 / 4.74 / 8.80 / 6.71
21
+ light ink oklch(0.50 0.130 238) 5.06 / 5.69 / 4.76 / 4.34 / 4.62 / 5.68
22
+ light lit ink oklch(0.55 0.160 238) 4.01 / 4.51 / 3.78 / 3.44 / 3.66 / 4.50
23
+ light ripple the ink's row
24
+ light keyline --bg on the --accent fill 4.09, on --accent-press 6.50
25
+ light refuse oklch(0.52 0.190 15) 5.36 --bg, 6.02 --surface-1
26
+ light trail core oklch(0.42 0.120 238) 7.10 / 7.98 / 6.68 / 6.10 / 6.48 / 7.97
27
+ light trail edge --accent 4.09 / 4.60 / 3.85 / 3.52 / 3.74 / 4.60
28
+ light spray oklch(0.45 0.130 238) 6.21 / 6.98 / 5.85 / 5.33 / 5.67 / 6.98
29
+ light spray hot oklch(0.50 0.140 39) 5.61 / 6.30 / 5.28 / 4.81 / 5.12 / 6.30
30
+ The tube paints at 0.9 alpha, 0.6 over text; composited on --surface-3, its
31
+ worst surface, `node tools/contrast.mjs "<fg> / 0.9" "<surface-3>"`: dark
32
+ core 4.76, edge 3.95; light core 5.01, edge 3.10. Over the accent fill the trail core
33
+ reads 1.22 dark and 1.73 light, as v2's comet reads 1.00 / 1.24:
34
+ the mark and the droplets carry the page keyline there, the tube does not.
35
+ The bloom is the glow ink at 0.105 at full speed on its own node, blended
36
+ `screen` in dark and `darken` in light, the halo's two directions, so it
37
+ lightens what it covers and never shades it. Its peak is the halo's bound:
38
+ the highest opacity at which no text pair reads lower than under the ring's
39
+ 12% hover fill, the bloom and the fill each blended over the glyph and its
40
+ ground alike, from the tool's exports. Dark holds to 0.105, where --accent
41
+ on --surface-3 reads 3.687 against its floor of 3.667 and --text-4 on
42
+ --surface-1 3.987 against 3.878 (plain 4.729). Light, where the ring fills with
43
+ --cursor-ink, holds to 0.480, where --on-accent on --accent reads 3.966
44
+ against 3.967; at 0.105 --text-4 on --surface-1 reads 5.380 against 4.772
45
+ (plain 5.555).
46
+ ============================================================ */
47
+
48
+ #bg-dots { position: fixed; inset: 0; z-index: -1; pointer-events: none; }
49
+
50
+ /* `--cursor-v3` is the module's proof that this sheet applies: no colour token a page may
51
+ reset, and declared by no v1 or v2 file */
52
+ :root {
53
+ --cursor-v3: 1;
54
+ --cursor-ink: var(--accent);
55
+ --cursor-lit: oklch(0.90 0.080 238);
56
+ --cursor-glow: oklch(0.90 0.080 238);
57
+ --cursor-glow-blend: screen;
58
+ --cursor-keyline: var(--bg);
59
+ --cursor-refuse: var(--danger);
60
+ --cursor-ripple: oklch(0.82 0.115 238);
61
+ --cursor-trail-core: oklch(0.82 0.115 238);
62
+ --cursor-trail-edge: var(--accent);
63
+ --cursor-spray: oklch(0.88 0.070 238);
64
+ --cursor-spray-hot: oklch(0.80 0.130 39);
65
+ --cursor-size-idle: 18px;
66
+ --cursor-size-hover: 24px;
67
+ --cursor-size-press: 19px;
68
+ --cursor-size-press-idle: 14px;
69
+ --cursor-size-blocked: 24px;
70
+ }
71
+ [data-theme="light"] {
72
+ --cursor-ink: oklch(0.50 0.130 238);
73
+ --cursor-lit: oklch(0.55 0.160 238);
74
+ --cursor-glow: oklch(0.89 0.070 238);
75
+ --cursor-glow-blend: darken;
76
+ --cursor-keyline: var(--bg);
77
+ --cursor-refuse: var(--danger);
78
+ --cursor-ripple: oklch(0.50 0.130 238);
79
+ --cursor-trail-core: oklch(0.42 0.120 238);
80
+ --cursor-trail-edge: var(--accent);
81
+ --cursor-spray: oklch(0.45 0.130 238);
82
+ --cursor-spray-hot: oklch(0.50 0.140 39);
83
+ }
84
+
85
+ /* The host is its own view-transition group: the theme swap would otherwise
86
+ freeze the cursor inside the root's old snapshot while the live one fades in,
87
+ so for the crossfade there would be two cursors and a ghost. It never fades
88
+ as a whole: the mark fades on the canvas, so a droplet still flying when the
89
+ pointer leaves the window plays out. */
90
+ #cursor {
91
+ position: fixed;
92
+ inset: 0;
93
+ z-index: 10000;
94
+ pointer-events: none;
95
+ view-transition-name: cursor;
96
+ }
97
+ #cursor-paint { position: absolute; top: 0; left: 0; }
98
+ ::view-transition-old(cursor) { display: none; }
99
+ ::view-transition-new(cursor) { animation: none; }
100
+
101
+ /* The light-up's bloom, under the mark: a 68px box scaled to the speed, its
102
+ blend the one direction per theme that cannot darken what it covers, as the
103
+ legacy halo blends. cursor.js places and fades it every frame. */
104
+ #cursor-glow {
105
+ position: fixed;
106
+ top: 0;
107
+ left: 0;
108
+ z-index: 9989;
109
+ width: 68px;
110
+ height: 68px;
111
+ pointer-events: none;
112
+ opacity: 0;
113
+ mix-blend-mode: var(--cursor-glow-blend);
114
+ background: radial-gradient(circle closest-side, var(--cursor-glow), transparent);
115
+ }
116
+
117
+ /* A cache can serve another version's cursor.js beside this file. A v1 or v2 module arms
118
+ `custom-cursor` for a mark this sheet does not style, so the native cursor hides only for
119
+ a v3 mark: under `cursor-v3`, which only this sheet's own module sets, or beside
120
+ `#cursor-paint`, which an older v3 module builds without setting the class. The `:has()`
121
+ guard is a rule of its own: an engine without `:has()` drops the whole selector list. Both
122
+ hide only inside the module's own gate query, where the mark can show, and the rule below
123
+ hides #cursor outside it: whatever a module does with `custom-cursor` when the gate changes,
124
+ the native cursor and the mark never hide together. */
125
+ @media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
126
+ html.custom-cursor.cursor-v3, html.custom-cursor.cursor-v3 * { cursor: none !important; -webkit-user-drag: none; }
127
+ html.custom-cursor:has(#cursor-paint), html.custom-cursor:has(#cursor-paint) * { cursor: none !important; -webkit-user-drag: none; }
128
+ }
129
+ /* a v1 or v2 module's ring, comet and halo stay out of the page flow beside this sheet:
130
+ unstyled, the comet's viewport-sized canvas and the halo would push the page taller */
131
+ #cursor-ring, #cursor-comet, #cursor-halo { display: none !important; }
132
+ /* a native drag ghost carries its own OS cursor, class or no class */
133
+ a, img { -webkit-user-drag: none; }
134
+ @media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) { #cursor, #cursor-glow { display: none; } }