reviewzy 0.3.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.
@@ -2,9 +2,11 @@
2
2
  const UNUSABLE = /::|&/; // a pseudo-element is a box closest() can never return
3
3
  const STATE_PSEUDO = /:(?:hover|active|focus|focus-visible|focus-within|visited|link|target)\b/g;
4
4
 
5
- // clickable with no cursor declaration anywhere: the browser draws the pointer itself
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.
6
8
  export const SEMANTIC_POINTER = [
7
- 'a[href]', 'area[href]', 'button', 'summary', 'select', 'label[for]',
9
+ 'a[href]', 'area[href]', 'button', 'summary', 'select',
8
10
  'input[type="checkbox"]', 'input[type="radio"]', 'input[type="range"]',
9
11
  'input[type="color"]', 'input[type="file"]', 'input[type="submit"]',
10
12
  'input[type="reset"]', 'input[type="button"]',
@@ -29,30 +31,36 @@ export function splitSelectorList(text) {
29
31
  return parts.map(s => s.trim()).filter(Boolean);
30
32
  }
31
33
 
32
- /* Every selector any rule gives one of `values` for `prop`, de-duplicated. It NARROWS a
33
- walk, never decides one: the computed style still has the last word, so a selector
34
- whose rule lives under a media query the viewport does not match costs one match and
35
- nothing else. A state pseudo is stripped so `.x:hover { position: sticky }` still names
36
- `.x`. The alternative, reading `getComputedStyle` for every element in the document,
37
- forces rendering inside any `content-visibility` subtree it crosses, which Chrome
38
- reports and this system's collapsed rows all carry. */
39
- export function collectPropSelectors(sheets, prop, values) {
40
- const found = new Set();
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) => {
41
39
  const visit = rules => {
42
40
  for (const rule of rules || []) {
43
- if (rule.selectorText && rule.style && values.includes(rule.style[prop])) {
44
- for (const part of splitSelectorList(rule.selectorText)) {
45
- if (UNUSABLE.test(part)) continue;
46
- const clean = part.replace(STATE_PSEUDO, '').trim();
47
- if (clean) found.add(clean);
48
- }
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);
49
46
  }
50
47
  if (rule.cssRules) visit(rule.cssRules); // @media, @supports, nesting
51
48
  }
52
49
  };
53
50
  for (const sheet of sheets || []) {
54
- try { visit(sheet.cssRules); } catch { /* cross-origin sheet */ }
51
+ try { visit(sheet.cssRules); } catch { /* cross-origin sheet, nothing worth a broken boot */ }
55
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));
56
64
  return [...found];
57
65
  }
58
66
 
@@ -63,21 +71,9 @@ export function collectCursorSelectors(sheets) {
63
71
  const bucketFor = c => c === 'pointer' ? buckets.pointer
64
72
  : c === 'default' ? buckets.inert
65
73
  : c === 'not-allowed' ? buckets.blocked : null;
66
- const visit = rules => {
67
- for (const rule of rules || []) {
68
- const bucket = rule.selectorText && rule.style ? bucketFor(rule.style.cursor) : null;
69
- if (bucket) for (const part of splitSelectorList(rule.selectorText)) {
70
- if (UNUSABLE.test(part)) continue;
71
- const clean = part.replace(STATE_PSEUDO, '').trim(); // folds `.x{default} .x:hover{pointer}`
72
- if (!clean) continue;
73
- for (const b of Object.values(buckets)) b.delete(clean); // document order stands in for the cascade
74
- bucket.add(clean);
75
- }
76
- if (rule.cssRules) visit(rule.cssRules); // @media, @supports, CSS nesting
77
- }
78
- };
79
- for (const sheet of sheets || []) {
80
- try { visit(sheet.cssRules); } catch { /* cross-origin sheet, nothing worth a broken boot */ }
81
- }
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
+ });
82
78
  return { pointer: [...buckets.pointer], inert: [...buckets.inert], blocked: [...buckets.blocked] };
83
79
  }
@@ -1,105 +1,134 @@
1
1
  /* ============================================================
2
- the ambient layer
3
- The dot grid and the pointer ring. Load after components.css;
4
- cursor.js boots both.
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).
5
46
  ============================================================ */
6
47
 
7
- /* ---- Cursor-reactive dot-grid background (sanctioned ambient; desktop only) ----
8
- Canvas created and painted on demand by cursor.js; sits behind all content via
9
- background propagation. Solid cards keep it to negative space. */
10
48
  #bg-dots { position: fixed; inset: 0; z-index: -1; pointer-events: none; }
11
49
 
12
- /* ---- Trailing pointer ring (desktop fine-pointer only) ----
13
- ONE node is the whole cursor. It TIGHTENS over a target rather than growing, which keeps it
14
- off the small ones, and over text it morphs into the caret bar itself. No dot in the middle
15
- and no second node for the caret: another mark under the pointer competes with the thing
16
- being pointed at, and a fade between two nodes throws away the morph.
17
- Five states: idle, `.hover`, `.press`, `.blocked` over anything refusing the click, and
18
- `.text` over a glyph run or a text field. `.text` is gated by a hit test against the glyphs,
19
- never by tag name (see the JS).
20
- EVERY STATE IS A WIDTH/HEIGHT CHANGE, never `transform: scale()`: a scaled 1px border
21
- renders sub-pixel, so a ring that tightens by scale fades as it tightens. The ring is fixed
22
- and childless, so nothing here touches page layout. */
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. */
23
90
  #cursor {
24
91
  position: fixed;
25
- top: 0; left: 0;
92
+ inset: 0;
26
93
  z-index: 10000;
27
94
  pointer-events: none;
28
- opacity: 0;
29
- transition: opacity var(--dur-near) var(--ease);
30
- --caret-h: 20px;
95
+ view-transition-name: cursor;
31
96
  }
32
- #cursor.visible { opacity: 1; }
97
+ #cursor-paint { position: absolute; top: 0; left: 0; }
98
+ ::view-transition-old(cursor) { display: none; }
99
+ ::view-transition-new(cursor) { animation: none; }
33
100
 
34
- #cursor-ring {
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 {
35
105
  position: fixed;
36
- top: 0; left: 0;
37
- width: 26px; height: 26px;
38
- border: 1px solid var(--accent);
39
- border-radius: 50%;
40
- will-change: transform;
41
- /* AN ACCENT HAIRLINE IS INVISIBLE OVER THE ACCENT FILLS - buttons, active chips, the sidebar
42
- cursor, bar fills - and those are exactly the things worth pointing at, so a keyline in the
43
- PAGE BACKGROUND rides just outside it. Whatever is underneath, exactly one of the two reads:
44
- on the page the keyline is the background and only the hairline shows, on an accent fill the
45
- hairline is the fill and only the keyline shows. It follows the shape through the caret
46
- morph, and it is the surface colour rather than a dark one for the light theme's sake, where
47
- a dark edge reads as a smudge and swallows the 1px line it was meant to protect. */
48
- box-shadow: 0 0 0 1px var(--bg);
49
- transition: width var(--dur-near) var(--ease), height var(--dur-near) var(--ease),
50
- border-radius var(--dur-near) var(--ease), border-color var(--dur-tint) var(--ease),
51
- background var(--dur-tint) var(--ease);
52
- }
53
-
54
- /* 4px per state: the tighten only has to be legible next to the state before it, and a deep one
55
- reads as the ring flinching away from the target. The fill jumps as well, because size alone
56
- stops separating press from hover once the steps are this small. */
57
- #cursor.hover #cursor-ring { width: 22px; height: 22px; background: color-mix(in srgb, var(--accent) 12%, transparent); }
58
- #cursor.hover.press #cursor-ring { width: 18px; height: 18px; background: color-mix(in srgb, var(--accent) 26%, transparent); }
59
-
60
- /* refuses the click: holds idle size on purpose. Not doing the tighten is the signal */
61
- #cursor.blocked #cursor-ring { border-color: var(--danger); }
62
-
63
- /* the ring IS the caret over text: the circle squeezes into the bar, and the transparent border
64
- lets the fill through the full 2px. The JS drops the trail here, so it stands under the
65
- pointer, where the click will put the real caret. */
66
- #cursor.text #cursor-ring {
67
- width: 2px; height: var(--caret-h);
68
- border-radius: 1px;
69
- border-color: transparent;
70
- background: var(--accent);
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);
71
115
  }
72
116
 
73
- /* one-shot on release over a target, removed on animationend. Placed by left/top so the keyframe
74
- owns the transform outright. Carries the ring's keyline: it fires over targets, and those are
75
- the fills it would vanish into */
76
- .cursor-pulse {
77
- position: fixed;
78
- width: 26px; height: 26px;
79
- margin: -13px 0 0 -13px;
80
- border: 1px solid var(--accent);
81
- border-radius: 50%;
82
- box-shadow: 0 0 0 1px var(--bg);
83
- will-change: transform;
84
- animation: cursor-pulse var(--dur-far) var(--ease-out) forwards;
85
- }
86
- /* Starts at 6.2px across, near enough to a point that the ripple reads as leaving the click
87
- rather than as the pressed ring let go. The transform scales the stroke with it, so a 1px
88
- border would draw a quarter-pixel line at that size and the small start would not paint at
89
- all; the border-width runs the other way so the start still draws. Sampled every 2ms across
90
- the run, the drawn stroke spans 0.96 to 2.6px, against 0.24px at the start without it. The
91
- spread is Chrome flooring the animated border-width to whole pixels. */
92
- @keyframes cursor-pulse {
93
- from { transform: scale(0.24); border-width: 4px; opacity: 0.85; }
94
- to { transform: scale(2.1); border-width: 0.5px; opacity: 0; }
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; }
95
128
  }
96
-
97
- /* `-webkit-user-drag`: a native drag puts the OS cursor and a ghost image back on screen, and
98
- neither obeys `cursor: none`. The JS cancels dragstart too, for the engines ignoring this,
99
- and refuses the pointerdown on a link, which is earlier than either.
100
- Selection survives - it stops elements being dragged, not text being picked. */
101
- html.custom-cursor, html.custom-cursor * { cursor: none !important; -webkit-user-drag: none; }
102
- /* Unscoped: a native drag ghost carries its own OS cursor and defeats the ring
103
- the moment it starts, class or no class. Selection survives. */
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 */
104
133
  a, img { -webkit-user-drag: none; }
105
- @media (hover: none), (pointer: coarse) { #cursor { display: none; } }
134
+ @media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) { #cursor, #cursor-glow { display: none; } }