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.
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/dashboard/mount.ts +20 -3
- package/src/dashboard/static/components.css +1196 -451
- package/src/dashboard/static/cursor-rules.js +30 -34
- package/src/dashboard/static/cursor.css +118 -89
- package/src/dashboard/static/cursor.js +1636 -245
- package/src/dashboard/static/dashboard.css +275 -179
- package/src/dashboard/static/dashboard.js +330 -185
- package/src/dashboard/static/tokens.css +240 -54
- package/src/dashboard/static/ui.js +666 -171
- package/src/dashboard/views.tsx +1138 -841
- package/src/dashboard/word-diff.ts +18 -25
- package/src/db/migrations.ts +5 -6
- package/src/db/queries.ts +37 -0
- package/src/mcp/file-entries.ts +1 -1
- package/src/mcp/list-entries.ts +11 -2
|
@@ -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',
|
|
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
|
-
/*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
67
|
-
for (const
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
4
|
-
|
|
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
|
-
/*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
92
|
+
inset: 0;
|
|
26
93
|
z-index: 10000;
|
|
27
94
|
pointer-events: none;
|
|
28
|
-
|
|
29
|
-
transition: opacity var(--dur-near) var(--ease);
|
|
30
|
-
--caret-h: 20px;
|
|
95
|
+
view-transition-name: cursor;
|
|
31
96
|
}
|
|
32
|
-
#cursor
|
|
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
|
-
|
|
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;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
/*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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; } }
|