linear-grab-bridge 0.25.2 → 0.26.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.
@@ -27,7 +27,7 @@ import {
27
27
  writeFileSync,
28
28
  } from 'node:fs';
29
29
  import { homedir, platform, tmpdir } from 'node:os';
30
- import { join } from 'node:path';
30
+ import { join, sep } from 'node:path';
31
31
 
32
32
  const argv = process.argv.slice(2);
33
33
  const flag = (name, fallback) => {
@@ -37,7 +37,7 @@ const flag = (name, fallback) => {
37
37
  const PORT = Number(flag('--port', '4577'));
38
38
  const DIR = flag('--dir', process.cwd());
39
39
  const CLAUDE_BIN = flag('--claude', 'claude');
40
- const VERSION = '0.25.2';
40
+ const VERSION = '0.26.0';
41
41
 
42
42
  // ---- audit subcommand dispatch ---------------------------------------------
43
43
  // `npx linear-grab-bridge audit` is a headless design gate — it sweeps a
@@ -783,6 +783,44 @@ const server = createServer(async (req, res) => {
783
783
  interactions: interactions.slice(-20),
784
784
  });
785
785
  }
786
+ // Serve the repo's render-rulebook markdown to the panel — lets the panel
787
+ // surface the relevant R-rule section alongside a render-scan finding.
788
+ // ?doc=render (default) is the only supported value; others get ok:false.
789
+ if (req.method === 'GET' && url.pathname === '/scan/rulebook') {
790
+ const doc = url.searchParams.get('doc') ?? 'render';
791
+ if (doc !== 'render') return json(res, 200, { ok: false, error: 'unknown doc' });
792
+
793
+ // Resolve the rulebook path: .lineargrab.json key "renderRulebook" wins,
794
+ // else fall back to the canonical filename in the repo root.
795
+ let relPath = 'React-rerender-primitives.md';
796
+ try {
797
+ const cfg = JSON.parse(readFileSync(join(DIR, '.lineargrab.json'), 'utf8'));
798
+ if (typeof cfg.renderRulebook === 'string' && cfg.renderRulebook.trim()) {
799
+ relPath = cfg.renderRulebook.trim();
800
+ }
801
+ } catch {
802
+ /* missing or invalid config — use the default */
803
+ }
804
+
805
+ // Reject absolute paths supplied in the config value.
806
+ if (relPath.startsWith('/') || /^[A-Za-z]:[/\\]/.test(relPath)) {
807
+ return json(res, 400, { ok: false, error: 'path outside repo' });
808
+ }
809
+
810
+ // Reject resolved paths that escape the repo root (traversal guard).
811
+ const resolved = join(DIR, relPath);
812
+ if (!resolved.startsWith(DIR + sep) && resolved !== DIR) {
813
+ return json(res, 400, { ok: false, error: 'path outside repo' });
814
+ }
815
+
816
+ let text;
817
+ try {
818
+ text = readFileSync(resolved, 'utf8');
819
+ } catch {
820
+ return json(res, 200, { ok: false, error: 'not found' });
821
+ }
822
+ return json(res, 200, { ok: true, text, path: resolved });
823
+ }
786
824
  // Reset the staging branch: delete + recreate from the default branch.
787
825
  if (req.method === 'POST' && url.pathname === '/branch/reset') {
788
826
  const body = await readBody(req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-grab-bridge",
3
- "version": "0.25.2",
3
+ "version": "0.26.0",
4
4
  "description": "Local bridge for Linear Grab — delegate issues from the browser panel to headless Claude Code sessions running in your repo, with live status and an upload relay.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
1
  (function(){function e(e){return e.split(`,`).map(e=>{let t=e.trim();return t.endsWith(`ms`)?parseFloat(t)||0:t.endsWith(`s`)?(parseFloat(t)||0)*1e3:0})}var t=new Set(`width.height.min-width.min-height.max-width.max-height.top.left.right.bottom.inset.margin.margin-top.margin-right.margin-bottom.margin-left.padding.padding-top.padding-right.padding-bottom.padding-left.font-size.line-height.letter-spacing.box-shadow.filter.background-position.grid-template-columns.grid-template-rows.flex-basis.gap`.split(`.`));function n(e){return e.includes(`all`)?[`all (includes layout/paint props)`]:e.filter(e=>t.has(e))}function r(e){let t=i(e);if(t===`ease-in`||t===`ease-in-out`)return t;let n=t.match(/cubic-bezier\(\s*([\d.]+)\s*,\s*(-?[\d.]+)/);if(n){let e=parseFloat(n[1]),r=parseFloat(n[2]);if(e>=.4&&r<e/2)return t}return null}function i(e){let t=0;for(let n=0;n<e.length;n++){let r=e[n];if(r===`(`)t++;else if(r===`)`)t--;else if(r===`,`&&t===0)return e.slice(0,n).trim()}return e.trim()}var a=`[data-slot*="content"], [role="menu"], [role="dialog"], [role="listbox"]`,o=new Set([`width`,`height`,`flex-basis`,`grid-template-columns`,`grid-template-rows`,`grid-template-areas`]);function s(e,t){if(e instanceof SVGElement)return!0;try{if(e.closest(`svg`))return!0}catch{}try{if(e.closest(a))return!0}catch{}try{if(e.closest(`[data-panel]`))return!t||t.length===0||t.every(e=>o.has(e))}catch{}return!1}function c(e){let t=e.id?`#${e.id}`:``,n=typeof e.className==`string`&&e.className?`.${e.className.split(/\s+/).filter(Boolean).slice(0,2).join(`.`)}`:``;return`${e.tagName.toLowerCase()}${t}${n}`}function l(e,t,n){return{ruleId:e.id,part:e.part,severity:e.severity,description:e.description,selector:c(t),evidence:n,el:typeof WeakRef<`u`?new WeakRef(t):void 0}}function u(e){return e.getBoundingClientRect()}function d(e){return e.split(`/`)[0].trim().split(/\s+/).filter(Boolean).map(e=>e.endsWith(`px`)?parseFloat(e):NaN)}function f(e){let t=e.match(/rgba?\(([^)]+)\)/);return t?t[1].split(`,`).map(e=>e.trim()).join(`,`):null}function p(e,t){let n=t,r=n.maskImage||n.webkitMaskImage||``;if(r&&r!==`none`)return r;let i=(e.getAttribute(`style`)??``).match(/(?:^|;|\s)(?:-webkit-)?mask-image\s*:\s*([^;]+)/i);return i&&i[1].trim()&&i[1].trim()!==`none`?i[1].trim():``}var m=new Set([0,8,12,16]),h={light:f(`rgb(240, 240, 241)`),dark:f(`rgb(35, 35, 37)`)},g=new Set([`#50e3c2`,`#ee0000`,`#f5a623`]),_=`button, a, [role="button"], [role="tab"], [role="menuitem"], [role="option"], input, textarea, select`;function v(e){let t=[],n=e=>{for(let r of Array.from(e)){let e=r;if(e.cssRules&&e.cssRules.length){n(e.cssRules);continue}let i=r.selectorText;i&&i.includes(`:hover`)&&t.push(i.replace(/:hover/g,``))}};for(let t of Array.from(e.styleSheets))try{n(t.cssRules)}catch{}return t}function y(e,t){for(let n of t)if(n.trim())try{if(e.matches(n))return!0}catch{}return!1}function b(e){return!e.matches(`button, [role="button"]`)||e.querySelectorAll(`svg`).length===0?!1:(e.textContent??``).trim().length===0}function x(e){let t=e.parentElement;for(;t;){try{let e=S(t).overflowY;if(e===`auto`||e===`scroll`)return t}catch{}t=t.parentElement}return null}var S=e=>getComputedStyle(e),C={id:`radius-vocabulary`,part:`§40`,severity:`error`,description:`§40: radius vocabulary is {8, 12, 16, full} — never rounded-md/[4px]/[6px]/[10px]. Off-vocabulary radius.`,check(e){let t=[];for(let n of e.els){let r=e.css(n).borderRadius;if(!r||r===`0px`)continue;let i=d(r);if(i.some(e=>Number.isNaN(e)))continue;let a=Math.max(...i);if(a===0)continue;let o=u(n),s=Math.min(o.width||0,o.height||0);s>0&&a>=s/2-.5||i.every(e=>m.has(e))||t.push(l(this,n,`border-radius: ${r}`))}return t}},w={id:`row-radius-token`,part:`§40`,severity:`error`,description:`§40: list rows consume --row-radius (8px) or 0 — no bespoke per-surface row radius.`,check(e){let t=[];for(let n of e.els){let r=u(n).height;if(r<26||r>50||!x(n))continue;let i=n.parentElement;if(!i||Array.from(i.children).filter(e=>{let t=u(e);return Math.abs(t.height-r)<2}).length<3)continue;let a=d(e.css(n).borderRadius);if(a.some(e=>Number.isNaN(e)))continue;let o=Math.max(0,...a);o===0||o===8||t.push(l(this,n,`row radius ${e.css(n).borderRadius} (want 0 or var(--row-radius)=8px)`))}return t}},T={id:`active-fill-token`,part:`§11`,severity:`error`,description:`§11: active/selected fill must be var(--selected) (theme-safe) — never bg-secondary (#F0F0F1 / #232325).`,check(e){let t=[],n=null;try{let t=e.root.documentElement??document.documentElement;t&&(n=f(e.css(t).getPropertyValue(`--selected`).trim()))}catch{}let r=[h.light,h.dark].filter(Boolean);if(n&&r.includes(n)){let t=e.root.documentElement??document.documentElement;return[l({id:this.id,part:this.part,severity:`warn`,description:`§11: active-fill-token unverifiable — --selected resolves EQUAL to bg-secondary this theme; cannot distinguish the token from the anti-pattern.`},t??e.els[0],`--selected == bg-secondary (${n})`)]}for(let n of e.els){if(!(n.matches(`[role="tab"], [role="button"], [aria-selected], [role="option"]`)||n.closest(`[role="tablist"], [role="menu"], [data-rail], nav`)))continue;let i=f(e.css(n).backgroundColor);i&&r.includes(i)&&t.push(l(this,n,`background bg-secondary (${e.css(n).backgroundColor}) — use var(--selected)`))}return t}},E={id:`inline-style-state`,part:`§40`,severity:`error`,description:`§40: the shared row-action class owns ALL visual states; inline styles carry LAYOUT ONLY — an inline background/color kills the class state.`,check(e){let t=[];for(let n of e.els){let e=typeof n.className==`string`?n.className:``;if(!/pb-row-action|row-action/.test(e))continue;let r=n.getAttribute(`style`)??``;/(^|;|\s)(background|color)\s*:/.test(r)&&t.push(l(this,n,`inline style="${r}" on a row-action element`))}return t}},D={id:`icon-button-no-hover`,part:`§40`,severity:`warn`,description:`§40/Part 10: every interactive control changes color on hover — icon-only button with no :hover rule and no .pb-row-action.`,check(e){let t=[],n=v(e.root.styleSheets?e.root:document);for(let r of e.els){if(!b(r))continue;let i=typeof r.className==`string`?r.className:``;/pb-row-action|row-action/.test(i)||r.hasAttribute(`data-framer-appear-id`)||Array.from(r.attributes).some(e=>e.name.startsWith(`data-framer`))||e.css(r).willChange.includes(`transform`)||y(r,n)||t.push(l(this,r,`icon-only ${r.tagName.toLowerCase()} with no hover rule / .pb-row-action`))}return t}},O={id:`icon-button-no-tooltip`,part:`§7`,severity:`warn`,description:`§7: every icon-only button gets the shared Tooltip — needs aria-describedby / data-slot="tooltip-trigger" (menu-row title exempt).`,check(e){let t=[];for(let n of e.els)b(n)&&(n.hasAttribute(`aria-describedby`)||n.getAttribute(`data-slot`)===`tooltip-trigger`||n.closest(`[data-slot="tooltip-trigger"]`)||n.closest(`[role="menuitem"]`)&&n.hasAttribute(`title`)||t.push(l(this,n,`icon-only ${n.tagName.toLowerCase()} with no tooltip trigger / aria-describedby`)));return t}},k={id:`unvirtualized-scroller`,part:`§0`,severity:`error`,description:`§0: every dynamic-length scrolling list virtualizes — a tall scroller with 60+ direct children is unvirtualized.`,check(e){let t=[];for(let n of e.els){let r=e.css(n);if(![r.overflowY,r.overflowX,r.overflow].some(e=>e===`auto`||e===`scroll`)||n.scrollHeight<=2*n.clientHeight)continue;let i=n.children.length;i<=60||t.push(l(this,n,`${i} direct children, scrollHeight ${n.scrollHeight} > 2×clientHeight ${n.clientHeight}`))}return t}},A={id:`scroll-no-fade`,part:`§1`,severity:`warn`,description:`§1: a horizontal scroller needs a truthful scroll fade — no scroll-fade-x/scroll-fade-x-js class and no mask-image.`,check(e){let t=[];for(let n of e.els){let r=e.css(n).overflowX;if(r!==`auto`&&r!==`scroll`||n.clientWidth<24||n.scrollWidth<=n.clientWidth+8)continue;let i=typeof n.className==`string`?n.className:``;/scroll-fade-x(-js)?/.test(i)||p(n,e.css(n))||t.push(l(this,n,`horizontal scroller (scrollWidth ${n.scrollWidth} > clientWidth ${n.clientWidth}) with no fade`))}return t}},j={id:`phantom-fade`,part:`§1`,severity:`warn`,description:`§1: a fade must NEVER paint when nothing is hidden — mask-image present but scrollWidth ≤ clientWidth (the fade lies).`,check(e){let t=[];for(let n of e.els)p(n,e.css(n))&&(n.scrollWidth>n.clientWidth||t.push(l(this,n,`mask-image fade but scrollWidth ${n.scrollWidth} ≤ clientWidth ${n.clientWidth}`)));return t}},M={id:`scrollbar-flush`,part:`Part 5`,severity:`warn`,description:`Part 5: scrollables carry asymmetric right padding so content clears the scrollbar — padding-right < 8px while overflowing vertically with content reaching the right edge.`,check(e){let t=[];for(let n of e.els){let r=e.css(n);if(![r.overflowY,r.overflow].some(e=>e===`auto`||e===`scroll`)||n.scrollHeight<=n.clientHeight)continue;let i=parseFloat(r.paddingRight)||0;if(i>=8)continue;let a=u(n);Array.from(n.children).some(e=>{let t=u(e);return a.right-t.right<=4})&&t.push(l(this,n,`padding-right ${i}px (<8px) with content flush to the scrollbar`))}return t}},N=/^("?(Geist|Geist Mono)"?|ui-monospace|ui-sans-serif|-apple-system|system-ui)/i,P={id:`font-family`,part:`Part 5`,severity:`warn`,description:`Part 5: Geist / Geist Mono ONLY — computed font-family does not start with the Geist / ui-monospace fallback chain.`,check(e){let t=[];for(let n of e.els){if(!q(n))continue;let r=e.css(n).fontFamily;r&&(N.test(r.trim())||t.push(l(this,n,`font-family: ${r}`)))}return t}},F={id:`uppercase-label`,part:`§25`,severity:`error`,description:`§25: group/section labels are sentence-case — never uppercase (text-transform: uppercase on small <13px text).`,check(e){let t=[];for(let n of e.els){if(!q(n))continue;let r=e.css(n);r.textTransform===`uppercase`&&((parseFloat(r.fontSize)||0)>=13||t.push(l(this,n,`text-transform: uppercase at ${r.fontSize}`)))}return t}},I={id:`hardcoded-status-hex`,part:`Part 5`,severity:`warn`,description:`Part 5: raw hex only for the status trio (#50E3C2/#EE0000/#F5A623) — a saturated non-token color on small UI text.`,check(e){let t=[],n=new Set;for(let e of g){let t=f(`rgb(${parseInt(e.slice(1,3),16)}, ${parseInt(e.slice(3,5),16)}, ${parseInt(e.slice(5,7),16)})`);t&&n.add(t)}try{let t=e.root,r=t.documentElement;if(r&&t.body){let i=e.css(r),a=t.createElement(`span`);a.style.display=`none`,t.body.appendChild(a);for(let e=0;e<i.length;e++){let t=i[e];if(!t.startsWith(`--`))continue;let r=i.getPropertyValue(t).trim();if(!r||r.length>40||(a.style.color=``,a.style.color=r,!a.style.color))continue;let o=f(getComputedStyle(a).color);o&&n.add(o)}a.remove()}}catch{}for(let r of e.els){if(!q(r))continue;let i=(r.getAttribute(`style`)??``).match(/#[0-9a-fA-F]{6}/g)??[],a=e.css(r);if((parseFloat(a.fontSize)||0)>=16)continue;for(let e of i)g.has(e.toLowerCase())||t.push(l(this,r,`hardcoded ${e} (not the brand status trio)`));let o=f(a.color);if(o&&i.length===0&&!n.has(o)){let[e,n,i]=o.split(`,`).map(e=>parseFloat(e));Number.isFinite(e)&&J(e,n,i)&&t.push(l(this,r,`saturated non-token color ${a.color} on small UI text`))}}return t}},L={id:`editor-shadow`,part:`Part 5`,severity:`error`,description:`Part 5: editor surfaces are FLAT — box-shadow that is not a 0.5px hairline ring / approved popover/card/input token shadow.`,check(e){let t=[];if(!(e.root.querySelector?.(`[data-editor-surface]`)||typeof location<`u`&&/\/editor\//.test(location.pathname)))return t;for(let n of e.els){if(!n.closest(`[data-editor-surface]`)&&!/\/editor\//.test(typeof location<`u`?location.pathname:``))continue;let r=e.css(n).boxShadow;!r||r===`none`||Y(r)||t.push(l(this,n,`box-shadow: ${r}`))}return t}},R={id:`focusring-clip`,part:`§24`,severity:`warn`,description:`§24/Part 5: focus rings never clip — a focusable element whose nearest overflow-hidden ancestor sits <4px from its border box.`,check(e){let t=[];for(let n of e.els){if(!n.matches(_)&&n.getAttribute(`tabindex`)==null)continue;let r=n.parentElement;for(;r;){let t=e.css(r);if([t.overflow,t.overflowX,t.overflowY].some(e=>e===`hidden`))break;r=r.parentElement}if(!r)continue;let i=u(n),a=u(r);if(i.width===0&&i.height===0)continue;let o=Math.min(i.left-a.left,a.right-i.right,i.top-a.top,a.bottom-i.bottom);o>=4||t.push(l(this,n,`overflow-hidden ancestor ${o.toFixed(1)}px from border box — focus ring will clip`))}return t}},z={id:`fixed-width-in-pane`,part:`§24`,severity:`warn`,description:`§24: no fixed field widths in resizable panes — w-[Npx] (N ≥ 200) inside a [data-panel] / flex-basis sibling clips at the pane edge.`,check(e){let t=[];for(let n of e.els){if(!n.closest(`[data-panel]`))continue;let e=typeof n.className==`string`?n.className:``,r=n.getAttribute(`style`)??``,i=e.match(/w-\[(\d+)px\]/)??r.match(/(?:^|;|\s)width\s*:\s*(\d+)px/);if(!i)continue;let a=parseFloat(i[1]);a<200||t.push(l(this,n,`fixed width ${a}px inside a resizable pane — use w-full max-w-[${a}px]`))}return t}},B={id:`menu-icons-all-or-none`,part:`§21`,severity:`error`,description:`§21: within one menu ALL items carry a leading icon or NONE do — a mixed set reads broken.`,check(e){let t=[],n=Array.from(e.root.querySelectorAll(`[role="menu"]`));for(let e of n){let n=Array.from(e.querySelectorAll(`[role="menuitem"]`));if(n.length<2)continue;let r=n.filter(e=>e.querySelector(`svg`)).length;r===0||r===n.length||t.push(l(this,e,`${r}/${n.length} menu items have a leading icon`))}return t}},V={id:`menu-icon-mixed-weight`,part:`§21`,severity:`warn`,description:`§21: menu icons are ONE stroke family — some filled (fill=currentColor) and some stroke-based, or rendered sizes differing >2px, mixes two design systems.`,check(e){let t=[],n=Array.from(e.root.querySelectorAll(`[role="menu"]`));for(let e of n){let n=Array.from(e.querySelectorAll(`[role="menuitem"] svg`));if(n.length<2)continue;let r=0,i=0,a=[];for(let e of n){Array.from(e.querySelectorAll(`path, circle, rect, polygon`)).some(e=>{let t=(e.getAttribute(`fill`)??``).toLowerCase();return t===`currentcolor`||t&&t!==`none`})?r++:i++;let t=u(e);a.push(Math.max(t.width,t.height))}let o=Math.max(...a),s=Math.min(...a),c=r>0&&i>0,d=o>0&&o-s>2;!c&&!d||t.push(l(this,e,c?`${r} filled + ${i} stroke-based icons in one menu`:`icon sizes differ by ${(o-s).toFixed(1)}px`))}return t}},H={id:`chrome-selectable`,part:`Part 5`,severity:`warn`,description:`Part 5: chrome (rails/pills/tabs) is select-none — a rail/pill/tab without user-select:none.`,check(e){let t=[];for(let n of e.els){if(!(n.matches(`[role="tab"]`)||n.closest(`[role="tablist"], [data-rail], nav`)||/\bpill\b/.test(typeof n.className==`string`?n.className:``)))continue;let r=e.css(n).userSelect||e.css(n).webkitUserSelect;r!==`none`&&t.push(l(this,n,`chrome element without user-select:none (user-select: ${r||`auto`})`))}return t}},U={id:`cursor-mismatch`,part:`Part 5`,severity:`warn`,description:`Part 5: cursor semantics — buttons get pointer, static text gets default. A button with cursor:text, or static text with cursor:pointer and no interactive ancestor.`,check(e){let t=[];for(let n of e.els){let r=e.css(n).cursor;if(n.matches(`button, [role="button"]`)&&r===`text`){t.push(l(this,n,`interactive control with cursor:text`));continue}if(r===`pointer`&&n.matches(`p, h1, h2, h3, h4, h5, h6, span`)){if(n.closest(`button, a, [role="button"], [onclick], label`)||n.matches(`button, a, [role="button"]`))continue;t.push(l(this,n,`static ${n.tagName.toLowerCase()} with cursor:pointer and no interactive ancestor`))}}return t}};function W(e){let t=e.transitionProperty.split(`,`).map(e=>e.trim()).filter(Boolean);return t.length===1&&t[0]===`none`?[]:t}var G=[C,w,T,E,D,O,k,A,j,M,P,F,I,L,R,z,B,V,H,U,{id:`transition-all`,part:`§42`,severity:`error`,description:`§42: never transition-all — scope to what changes (transition-colors / transition-[transform,...]).`,check(e){let t=[];for(let n of e.els){if(!n.matches(_))continue;let r=W(e.css(n));r.includes(`all`)&&(s(n,r)||t.push(l(this,n,`transition-property: all`)))}return t}},{id:`transition-paint-prop`,part:`§42`,severity:`error`,description:`§42: never transition layout/paint props (box-shadow, width, height, top/left, margin, padding) — they reflow/repaint every frame; focus rings SNAP, shadows animate a pseudo-element opacity.`,check(e){let t=[];for(let r of e.els){let i=W(e.css(r)),a=n(i).filter(e=>e!==`all (includes layout/paint props)`);a.length!==0&&(s(r,i)||t.push(l(this,r,`transitions layout/paint props: ${a.join(`, `)}`)))}return t}},{id:`transition-too-slow`,part:`§42`,severity:`error`,description:`§42: interaction feedback is ≤100ms (duration-75 for small controls) — the 100ms theme default is fine; an explicit duration-150+ crept in.`,check(t){let n=[];for(let r of t.els){if(!r.matches(_))continue;let i=t.css(r),a=W(i);if(a.length===0||s(r,a))continue;let o=e(i.transitionDuration),c=Math.max(0,...o);c<=100||n.push(l(this,r,`transition-duration ${c}ms (>100ms) on [${a.join(`, `)}]`))}return n}},{id:`ease-in-feedback`,part:`§42`,severity:`warn`,description:`§42: house curve is cubic-bezier(.2,0,.1,1) ease-out — never ease-in / ease-in-out (or an ease-in-shaped bezier) on feedback.`,check(e){let t=[];for(let n of e.els){if(!n.matches(_))continue;let i=e.css(n),a=W(i);if(a.length===0||s(n,a))continue;let o=r(i.transitionTimingFunction);o&&t.push(l(this,n,`transition-timing-function: ${o}`))}return t}},{id:`press-instant-rule-missing`,part:`§42`,severity:`error`,description:"§42: the global press-instant rule (globals.css) must exist — `button:active{transition-duration:0s}` (or transition:none). Absent = press feedback animates instead of snapping.",check(e){let t=e.root.styleSheets?e.root:document,n=!1,r=e=>{for(let t of Array.from(e)){if(n)return;let e=t;if(e.cssRules&&e.cssRules.length){r(e.cssRules);continue}let i=t,a=i.selectorText;if(a&&/:active\b/.test(a)&&/(^|[\s,])(button|a|\[role=("|')?(button|tab|menuitem|option)("|')?\])/i.test(a))try{let e=i.style.transitionDuration,t=i.style.transition;if(e===`0s`||/(^|\s)none(\s|$)/.test(t)||/\b0s\b/.test(t)){n=!0;return}}catch{}}};try{for(let e of Array.from(t.styleSheets)){if(n)break;try{r(e.cssRules)}catch{}}}catch{}if(n)return[];let i=e.root.documentElement??e.els[0]??document.documentElement;return[l(this,i,"no `button:active { transition-duration: 0s }` rule on this page")]}}],K=[];function q(e){for(let t of Array.from(e.childNodes))if(t.nodeType===3&&(t.textContent??``).trim().length>0)return!0;return!1}function J(e,t,n){let r=Math.max(e,t,n);return r<40?!1:r-Math.min(e,t,n)>60}function Y(e){return!!(/\b0px\s+0px\s+0px\s+0?\.?5px\b/.test(e)||/\b0px?\s+0px?\s+0px?\s+0?\.?5px\b/.test(e))}function X(e=typeof document<`u`?document:{}){let t=new Map,n=e=>{let n=t.get(e);return n||(n=getComputedStyle(e),t.set(e,n)),n};S=n;let r=Array.from(e.querySelectorAll(`*`)),i=[],a=e.documentElement??null,o=e.body??null,s=(a?.getBoundingClientRect().width??0)>0;for(let e of r){if(e===a||e===o||e.closest(`#linear-grab-root, #claude-agent-glow-border, [id^="react-scan"]`))continue;let t=u(e);if(s){if(t.width<2||t.height<2)continue}else if(t.width===0&&t.height===0&&e.children.length===0)continue;i.push(e)}let c={root:e,els:i,css:n},l=[];for(let e of[...G,...K])try{l.push(...e.check(c))}catch{}S=e=>getComputedStyle(e);let d=new Map;for(let e of l){let t=`${e.ruleId}|${e.selector}|${e.evidence}`,n=d.get(t);n?n.count=(n.count??1)+1:d.set(t,e)}return[...d.values()]}function Z(e){let t=new Map;for(let n of e){let e=t.get(n.ruleId);e?e.push(n):t.set(n.ruleId,[n])}let n=Array.from(t.entries()).sort((e,t)=>{let n=e[1][0].severity===`error`?0:1,r=t[1][0].severity===`error`?0:1;return n===r?t[1].length===e[1].length?e[0].localeCompare(t[0]):t[1].length-e[1].length:n-r}),r=e.length,i=e.filter(e=>e.severity===`error`).length,a=new Set(e.map(e=>e.page).filter(Boolean)).size>1,o=[`# Slop scan — ${i} error${i===1?``:`s`}, ${r-i} warn (${r} total)`,``];for(let[e,t]of n){let n=t[0],r=t.reduce((e,t)=>e+(t.count??1),0);o.push(`## ${e} — ${n.severity} ×${r} [${n.part}]`),o.push(n.description);for(let e of t){let t=e.component||e.source?` — ${[e.component,e.source].filter(Boolean).join(` @ `)}`:``,n=(e.count??1)>1?` ×${e.count}`:``;o.push(`- ${a&&e.page?`\`${e.page}\` `:``}\`${e.selector}\`${n} — ${e.evidence}${t}`)}o.push(``)}return o.join(`
2
2
  `)}function Q(e){return[`Below is a design-system SLOP report from my running app — each finding is a`,"live-DOM violation of the Linear/Protocolbase primitives contract (`Linear","-primitives.md`). Every rule cites its §id. Goal: zero errors. Rules:",``,`1. Fix at the TOKEN / VARIANT level, not the call sites. Most of these come`,` from shared row/button/menu variants — change --row-radius, --selected,`,` .pb-row-action, the Button/Input variant, or the globals.css §42 rules —`,` not one component at a time.`,`2. Cite the primitives §id in your fix (each finding carries it). Radius →`,` §40 vocabulary {8,12,16,full}; active fill → §11 var(--selected); menus →`,` §21 all-or-none icons + one stroke family; uppercase labels → §25;`,` editor flatness → Part 5; scrollers → §0–1 virtualize + truthful fade.`,`3. §42 group (transition-all / paint-prop / too-slow / ease-in / press-`,` instant): scope transitions to what changes, drop box-shadow/width/height`,` from transition lists, keep feedback ≤100ms (duration-75 small controls),`,` use ease-out (cubic-bezier(.2,0,.1,1)), and keep the global`," `button:active{transition-duration:0s}` press-instant rule.",`4. Do NOT touch EXEMPT intentional motion — popover/menu/dialog entry-exit,`,` svg icon micro-motion, and panel width/height transitions are §42-blessed.`,` The scan already excludes them; do not "fix" them.`,`5. 'unverifiable' warnings mean the token collapsed to the anti-pattern this`,` theme — verify the intent in BOTH themes, do not just silence it.`,"6. Findings carry `Component @ file:line` when the fiber source resolved.",` When one file:line repeats across many components it is the nearest-`,` debug-fiber fallback — locate the real code by the selector classes.`,`7. PROOF REQUIRED: after the change I will re-run the scan — it must show 0`,` errors. State what you changed per token/variant; do not claim done blind.`,``,`---`,``,Z(e)].join(`
3
- `)}function $(){return X(document).map(({el:e,...t})=>t)}async function ee(){let e=X(document),t=window.__REACT_GRAB__;if(t&&(t.getDisplayName||t.getSource)){let n=new Map;for(let t of e){let e=t.el?.deref();if(!e)continue;let r=n.get(e);r?r.push(t):n.set(e,[t])}let r=300;for(let[e,i]of n){if(r--<=0)break;let n=null,a=null;try{n=t.getDisplayName?.(e)??null}catch{}try{let n=await t.getSource?.(e);n?.filePath&&(a=`${n.filePath}${n.lineNumber==null?``:`:${n.lineNumber}`}`)}catch{}if(!(!n&&!a))for(let e of i)e.component=n,e.source=a}}return e.map(({el:e,...t})=>t)}window.__SLOP_SCAN__={version:`0.25.2`,run:$,runAttributed:ee,report:Z,prompt:Q}})();
3
+ `)}function $(){return X(document).map(({el:e,...t})=>t)}async function ee(){let e=X(document),t=window.__REACT_GRAB__;if(t&&(t.getDisplayName||t.getSource)){let n=new Map;for(let t of e){let e=t.el?.deref();if(!e)continue;let r=n.get(e);r?r.push(t):n.set(e,[t])}let r=300;for(let[e,i]of n){if(r--<=0)break;let n=null,a=null;try{n=t.getDisplayName?.(e)??null}catch{}try{let n=await t.getSource?.(e);n?.filePath&&(a=`${n.filePath}${n.lineNumber==null?``:`:${n.lineNumber}`}`)}catch{}if(!(!n&&!a))for(let e of i)e.component=n,e.source=a}}return e.map(({el:e,...t})=>t)}window.__SLOP_SCAN__={version:`0.26.0`,run:$,runAttributed:ee,report:Z,prompt:Q}})();