mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
extension: kinpaku UI for the popup, DevTools panel, sidebar and overlays (#729)
* extension: kinpaku UI for the popup, DevTools panel, sidebar and overlays The extension still wore magenta (#d6336c on the toolbar badge, oklch(48% 0.25 350) through the DevTools panel and the Elements sidebar) and a cream popup left over from the old dark system. It now wears the brand the site ships in impeccable-site PR #34: paper and instruments, one light theme. - extension/shared/kinpaku.css is the one token layer the three pages link. Values are copied from site/styles/kinpaku-tokens.css, and the header states the four rules that shape the rest: paper and ink, gold as jewelry, patina carries colored text, a dark surface is a control. - Severity has one language. An AI tell gets the detector's gold tag and a lit gold dot; a quality issue gets patina; a scan that did not run gets vermilion. Selectors and details are patina-deep, which clears 4.5:1 on paper, and gold never carries text anywhere. - Popup: neutral paper, the mark as a carved ink tile, an ink primary button and a paper cap for the secondary, and the count in ink over a rule that lights gold when there is something to report. - Panel and sidebar: paper in both DevTools themes, since the brand has no dark theme any more. The panel handles the seam instead of inverting, with a hard top edge under .theme-dark. Segmented controls and the switch are paper hardware: a recessed track, a raised cap, a lit gold dot. - Overlays in the page: a gold hairline plus a soft outer glow instead of a 2px outline, and the label chip is now the tag, ink on gold in the mono face, matching what live mode draws. - The toolbar badge is gold with dark ink text (about 11.8:1); Chrome's default white badge text does not clear 4.5:1 on gold. Behavior is untouched: every id, class hook and message the popup, panel, sidebar and content script depend on is unchanged, and the one markup change beyond the stylesheet links is a severity class on the sidebar's kind label. The extension smoke suite passes on all nine fixtures with no service worker or offscreen errors, `bun run test` is green with a local engine build, and `web-ext lint` reports the same two Firefox-only errors and the same fifteen warnings as origin/main. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * extension: guard setBadgeTextColor, and make the dark seam paint-only Two review findings from #729. setBadgeTextColor arrived in Chrome 110 and does not exist on Firefox's action API. `chrome.action.setBadgeTextColor?.({...}).catch(...)` still evaluates `.catch` on the undefined the optional call returns, so on a browser without the method updateBadge raised a TypeError that escaped into whatever asked for the badge update. Both the method and its return value are checked now, and tests/extension-build.test.mjs pins the shape: no `?.(...).catch`, an existence check around the call, a promise check before `.catch`, and the gold and ink values with the magenta one gone. The dark-DevTools seam is a fixed 2px line at the top of the sheet, above the sticky toolbar because that is what keeps it in place while the panel scrolls under it. It now sets pointer-events: none, so it is paint and nothing else and the toolbar's top row of pixels stays clickable. extension/shared/ joins the detector suite's trigger list, since the shared stylesheet is part of extension packaging. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * overlay: the page banner is a paper bar, not a gold fill The last place gold was still upholstery. A full-width gold bar across someone else's page is a fill, and the system holds gold to a mark, a line, a lit dot, or a tag. The banner is now paper with ink text, a gold hairline along its bottom edge, and the detector's lit gold dot at its head, the same indicator the panel puts on a section. Each page-level finding wears the tag the panel gives it: gold with ink for an AI tell, a quiet gray chip for a quality issue, with the detail beside it in ink rather than folded into the chip. The bar carries a shadow of its own so it still separates from a dark host page, and the standalone controls take muted ink now that they sit on paper instead of gold. crates/live/assets/detect-antipatterns-browser.js is the regenerated tracked artifact, so live mode and the site's detector get the same banner. Extension smoke: all nine fixtures pass, no service worker or offscreen errors. bun run test green with a local engine build. web-ext lint reports the same two Firefox-only errors and fifteen warnings as origin/main. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
87d8f6d686
commit
5ddcce2574
@@ -16,21 +16,41 @@
|
||||
function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } = {}) {
|
||||
// Kinpaku gold — pinned to the site's brand token (see
|
||||
// site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with
|
||||
// the picker's C.brand in skill/scripts/live-browser.js and the kit's
|
||||
// picker section in site/styles/kinpaku-kit.css.
|
||||
// the picker's C.brand in skill/scripts/live-browser.js, the extension's
|
||||
// extension/shared/kinpaku.css, and the kit's picker section in
|
||||
// site/styles/kinpaku-kit.css.
|
||||
//
|
||||
// One color across both light and dark host pages. The outline is a
|
||||
// 2px gesture pointing at an element + a labeled tag — it's a marker,
|
||||
// not body text, so it doesn't need WCAG AA against the page. The
|
||||
// label text inside the gold tag is dark (LABEL_INK) which has ~16:1
|
||||
// against the leaf gold, so reading the rule name is solid in both
|
||||
// modes. Hover deepens the gold (preserves chroma — never drops it,
|
||||
// One color across both light and dark host pages. The marker is a gold
|
||||
// hairline plus a soft outer glow: the hairline holds its shape on a
|
||||
// light page, the glow carries it on a dark one, and neither reads as
|
||||
// body text, so the pair doesn't need WCAG AA against the page. The
|
||||
// label is the detector's tag: dark ink (LABEL_INK, --ks-on-gold) on
|
||||
// leaf gold, about 11.8:1, in the mono face the tag uses everywhere
|
||||
// else. Hover deepens the gold (preserves chroma, never drops it:
|
||||
// dropping chroma washes the gold into a sand/olive tone).
|
||||
const BRAND_COLOR = 'oklch(84% 0.19 80.46)';
|
||||
const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)';
|
||||
const LABEL_INK = 'oklch(4% 0.004 95)';
|
||||
const LABEL_INK = 'oklch(14% 0.018 95)';
|
||||
const LABEL_BG = BRAND_COLOR;
|
||||
const OUTLINE_COLOR = BRAND_COLOR;
|
||||
const GLOW = '0 0 0 3px oklch(84% 0.19 80.46 / 0.20), 0 0 12px oklch(84% 0.19 80.46 / 0.45)';
|
||||
const GLOW_HOVER = '0 0 0 3px oklch(74% 0.18 80 / 0.28), 0 0 16px oklch(74% 0.18 80 / 0.6)';
|
||||
|
||||
// The page-level banner is a paper bar, not a gold one: gold is the mark,
|
||||
// the tag and the lit dot, and a full-width gold fill across someone's page
|
||||
// is the one place it was still upholstery. Paper with ink text, a gold
|
||||
// hairline along the bottom edge, a lit gold dot at the head, and each
|
||||
// finding wearing the same tag the panel gives it (gold for an AI tell,
|
||||
// gray for a quality issue). The bar carries its own shadow so it still
|
||||
// separates from a dark host page.
|
||||
const PAPER = 'oklch(97.8% 0 0)';
|
||||
const PAPER_INK = 'oklch(13% 0 0)';
|
||||
const PAPER_MUTED = 'oklch(46% 0 0)';
|
||||
const PAPER_GRAY = 'oklch(92% 0 0)';
|
||||
const GOLD_LINE = 'oklch(77% 0.13 82)';
|
||||
const BANNER_LIFT = '0 1px 0 oklch(13% 0 0 / 0.10), 0 6px 18px oklch(0% 0 0 / 0.35)';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
|
||||
const SANS = 'system-ui, -apple-system, "Segoe UI", sans-serif';
|
||||
|
||||
// Inject hover styles via CSS (more reliable than JS event listeners)
|
||||
const styleEl = document.createElement('style');
|
||||
@@ -41,9 +61,10 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
}
|
||||
.impeccable-overlay:not(.impeccable-banner) {
|
||||
pointer-events: none;
|
||||
outline: 2px solid ${OUTLINE_COLOR};
|
||||
outline: 1px solid ${OUTLINE_COLOR};
|
||||
box-shadow: ${GLOW};
|
||||
border-radius: 4px;
|
||||
transition: outline-color 0.15s ease;
|
||||
transition: outline-color 0.15s ease, box-shadow 0.15s ease;
|
||||
animation: impeccable-reveal 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
animation-play-state: paused;
|
||||
border-top-left-radius: 0;
|
||||
@@ -53,6 +74,7 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
}
|
||||
.impeccable-overlay.impeccable-hover {
|
||||
outline-color: ${BRAND_COLOR_HOVER};
|
||||
box-shadow: ${GLOW_HOVER};
|
||||
z-index: 100001 !important;
|
||||
}
|
||||
.impeccable-overlay.impeccable-hover .impeccable-label {
|
||||
@@ -104,9 +126,9 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
function updateSpotlightClipPath() {
|
||||
if (!spotlightBackdrop || !spotlightTarget) return;
|
||||
const r = spotlightTarget.getBoundingClientRect();
|
||||
// Match the overlay's outer edge: element rect + 4px (2px overlay offset + 2px outline width)
|
||||
const inset = 4;
|
||||
const radius = 6; // outline border-radius (4) + outline width (2)
|
||||
// Match the overlay's outer edge: element rect + 3px (2px overlay offset + 1px hairline)
|
||||
const inset = 3;
|
||||
const radius = 5; // outline border-radius (4) + hairline width (1)
|
||||
const x1 = r.left - inset;
|
||||
const y1 = r.top - inset;
|
||||
const x2 = r.right + inset;
|
||||
@@ -298,11 +320,12 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
position: 'absolute', bottom: '100%', left: '-2px',
|
||||
display: 'flex', alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em',
|
||||
fontSize: '10px', fontWeight: '600', letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase',
|
||||
color: LABEL_INK, lineHeight: '14px',
|
||||
background: LABEL_BG,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
borderRadius: '4px 4px 0 0',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
||||
borderRadius: '3px 3px 0 0',
|
||||
});
|
||||
|
||||
const textSpan = document.createElement('span');
|
||||
@@ -326,9 +349,9 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
cycleMode = true;
|
||||
|
||||
const btnStyle = {
|
||||
background: 'none', border: 'none', color: 'rgba(255,255,255,0.7)',
|
||||
background: 'none', border: 'none', color: 'oklch(14% 0.018 95 / 0.7)',
|
||||
fontSize: '11px', cursor: 'pointer', padding: '3px 4px',
|
||||
fontFamily: 'system-ui, sans-serif', lineHeight: '14px',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', lineHeight: '14px',
|
||||
pointerEvents: 'auto',
|
||||
};
|
||||
|
||||
@@ -419,8 +442,10 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
banner.className = 'impeccable-overlay impeccable-banner';
|
||||
Object.assign(banner.style, {
|
||||
position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000',
|
||||
background: LABEL_BG, color: LABEL_INK,
|
||||
fontFamily: 'system-ui, sans-serif', fontSize: '13px',
|
||||
background: PAPER, color: PAPER_INK,
|
||||
borderBottom: `1px solid ${GOLD_LINE}`,
|
||||
boxShadow: BANNER_LIFT,
|
||||
fontFamily: SANS, fontSize: '12px',
|
||||
display: 'flex', alignItems: 'center', pointerEvents: 'auto',
|
||||
height: '36px', overflow: 'hidden', maxWidth: '100vw',
|
||||
transform: 'translateY(-100%)',
|
||||
@@ -430,24 +455,52 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
banner.style.transform = 'translateY(0)';
|
||||
}));
|
||||
|
||||
// The detector's indicator, the same lit gold dot the panel puts on a
|
||||
// section head.
|
||||
const dot = document.createElement('span');
|
||||
Object.assign(dot.style, {
|
||||
flexShrink: '0', width: '7px', height: '7px', margin: '0 0 0 12px',
|
||||
borderRadius: '50%', background: BRAND_COLOR,
|
||||
boxShadow: '0 0 0 1px oklch(13% 0 0 / 0.12), 0 0 4px oklch(84% 0.19 80 / 0.6)',
|
||||
});
|
||||
banner.appendChild(dot);
|
||||
|
||||
// Scrollable findings area
|
||||
const scrollArea = document.createElement('div');
|
||||
Object.assign(scrollArea.style, {
|
||||
flex: '1', minWidth: '0', overflowX: 'auto', overflowY: 'hidden',
|
||||
display: 'flex', gap: '8px', alignItems: 'center',
|
||||
display: 'flex', gap: '12px', alignItems: 'center',
|
||||
padding: '0 12px', scrollSnapType: 'x mandatory',
|
||||
scrollbarWidth: 'none',
|
||||
});
|
||||
for (const f of findings) {
|
||||
const prefix = RULE_CATEGORY[f.type] === 'slop' ? '\u2726 ' : '';
|
||||
const tag = document.createElement('span');
|
||||
tag.textContent = `${prefix}${TYPE_LABELS[f.type] || f.type}: ${f.detail}`;
|
||||
Object.assign(tag.style, {
|
||||
background: 'rgba(255,255,255,0.15)', padding: '2px 8px',
|
||||
borderRadius: '3px', fontSize: '12px', fontFamily: 'ui-monospace, monospace',
|
||||
const isSlop = RULE_CATEGORY[f.type] === 'slop';
|
||||
const item = document.createElement('span');
|
||||
Object.assign(item.style, {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
||||
whiteSpace: 'nowrap', flexShrink: '0', scrollSnapAlign: 'start',
|
||||
});
|
||||
scrollArea.appendChild(tag);
|
||||
|
||||
const tag = document.createElement('span');
|
||||
tag.textContent = TYPE_LABELS[f.type] || f.type;
|
||||
Object.assign(tag.style, {
|
||||
display: 'inline-flex', alignItems: 'center', minHeight: '16px',
|
||||
padding: '0 6px', borderRadius: '3px',
|
||||
background: isSlop ? BRAND_COLOR : PAPER_GRAY,
|
||||
color: isSlop ? LABEL_INK : PAPER_MUTED,
|
||||
fontFamily: MONO, fontSize: '10px', fontWeight: '600',
|
||||
letterSpacing: '0.06em', lineHeight: '1', textTransform: 'uppercase',
|
||||
});
|
||||
|
||||
const detail = document.createElement('span');
|
||||
detail.textContent = f.detail;
|
||||
Object.assign(detail.style, {
|
||||
color: PAPER_INK, fontFamily: SANS, fontSize: '12px',
|
||||
});
|
||||
|
||||
item.appendChild(tag);
|
||||
item.appendChild(detail);
|
||||
scrollArea.appendChild(item);
|
||||
}
|
||||
banner.appendChild(scrollArea);
|
||||
|
||||
@@ -465,7 +518,7 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
toggle.title = 'Toggle overlay visibility';
|
||||
Object.assign(toggle.style, {
|
||||
background: 'none', border: 'none',
|
||||
color: 'white', fontSize: '16px', cursor: 'pointer', padding: '0 4px',
|
||||
color: PAPER_MUTED, fontSize: '16px', cursor: 'pointer', padding: '0 4px',
|
||||
opacity: '0.85', transition: 'opacity 0.15s',
|
||||
});
|
||||
let overlaysVisible = true;
|
||||
@@ -483,7 +536,7 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
close.title = 'Dismiss banner';
|
||||
Object.assign(close.style, {
|
||||
background: 'none', border: 'none',
|
||||
color: 'white', fontSize: '18px', cursor: 'pointer', padding: '0 4px',
|
||||
color: PAPER_MUTED, fontSize: '18px', cursor: 'pointer', padding: '0 4px',
|
||||
});
|
||||
close.addEventListener('click', () => banner.remove());
|
||||
controls.appendChild(close);
|
||||
|
||||
@@ -2785,21 +2785,41 @@ function createInPageVisualIO(wasm) {
|
||||
function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } = {}) {
|
||||
// Kinpaku gold — pinned to the site's brand token (see
|
||||
// site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with
|
||||
// the picker's C.brand in skill/scripts/live-browser.js and the kit's
|
||||
// picker section in site/styles/kinpaku-kit.css.
|
||||
// the picker's C.brand in skill/scripts/live-browser.js, the extension's
|
||||
// extension/shared/kinpaku.css, and the kit's picker section in
|
||||
// site/styles/kinpaku-kit.css.
|
||||
//
|
||||
// One color across both light and dark host pages. The outline is a
|
||||
// 2px gesture pointing at an element + a labeled tag — it's a marker,
|
||||
// not body text, so it doesn't need WCAG AA against the page. The
|
||||
// label text inside the gold tag is dark (LABEL_INK) which has ~16:1
|
||||
// against the leaf gold, so reading the rule name is solid in both
|
||||
// modes. Hover deepens the gold (preserves chroma — never drops it,
|
||||
// One color across both light and dark host pages. The marker is a gold
|
||||
// hairline plus a soft outer glow: the hairline holds its shape on a
|
||||
// light page, the glow carries it on a dark one, and neither reads as
|
||||
// body text, so the pair doesn't need WCAG AA against the page. The
|
||||
// label is the detector's tag: dark ink (LABEL_INK, --ks-on-gold) on
|
||||
// leaf gold, about 11.8:1, in the mono face the tag uses everywhere
|
||||
// else. Hover deepens the gold (preserves chroma, never drops it:
|
||||
// dropping chroma washes the gold into a sand/olive tone).
|
||||
const BRAND_COLOR = 'oklch(84% 0.19 80.46)';
|
||||
const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)';
|
||||
const LABEL_INK = 'oklch(4% 0.004 95)';
|
||||
const LABEL_INK = 'oklch(14% 0.018 95)';
|
||||
const LABEL_BG = BRAND_COLOR;
|
||||
const OUTLINE_COLOR = BRAND_COLOR;
|
||||
const GLOW = '0 0 0 3px oklch(84% 0.19 80.46 / 0.20), 0 0 12px oklch(84% 0.19 80.46 / 0.45)';
|
||||
const GLOW_HOVER = '0 0 0 3px oklch(74% 0.18 80 / 0.28), 0 0 16px oklch(74% 0.18 80 / 0.6)';
|
||||
|
||||
// The page-level banner is a paper bar, not a gold one: gold is the mark,
|
||||
// the tag and the lit dot, and a full-width gold fill across someone's page
|
||||
// is the one place it was still upholstery. Paper with ink text, a gold
|
||||
// hairline along the bottom edge, a lit gold dot at the head, and each
|
||||
// finding wearing the same tag the panel gives it (gold for an AI tell,
|
||||
// gray for a quality issue). The bar carries its own shadow so it still
|
||||
// separates from a dark host page.
|
||||
const PAPER = 'oklch(97.8% 0 0)';
|
||||
const PAPER_INK = 'oklch(13% 0 0)';
|
||||
const PAPER_MUTED = 'oklch(46% 0 0)';
|
||||
const PAPER_GRAY = 'oklch(92% 0 0)';
|
||||
const GOLD_LINE = 'oklch(77% 0.13 82)';
|
||||
const BANNER_LIFT = '0 1px 0 oklch(13% 0 0 / 0.10), 0 6px 18px oklch(0% 0 0 / 0.35)';
|
||||
const MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
|
||||
const SANS = 'system-ui, -apple-system, "Segoe UI", sans-serif';
|
||||
|
||||
// Inject hover styles via CSS (more reliable than JS event listeners)
|
||||
const styleEl = document.createElement('style');
|
||||
@@ -2810,9 +2830,10 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
}
|
||||
.impeccable-overlay:not(.impeccable-banner) {
|
||||
pointer-events: none;
|
||||
outline: 2px solid ${OUTLINE_COLOR};
|
||||
outline: 1px solid ${OUTLINE_COLOR};
|
||||
box-shadow: ${GLOW};
|
||||
border-radius: 4px;
|
||||
transition: outline-color 0.15s ease;
|
||||
transition: outline-color 0.15s ease, box-shadow 0.15s ease;
|
||||
animation: impeccable-reveal 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
animation-play-state: paused;
|
||||
border-top-left-radius: 0;
|
||||
@@ -2822,6 +2843,7 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
}
|
||||
.impeccable-overlay.impeccable-hover {
|
||||
outline-color: ${BRAND_COLOR_HOVER};
|
||||
box-shadow: ${GLOW_HOVER};
|
||||
z-index: 100001 !important;
|
||||
}
|
||||
.impeccable-overlay.impeccable-hover .impeccable-label {
|
||||
@@ -2873,9 +2895,9 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
function updateSpotlightClipPath() {
|
||||
if (!spotlightBackdrop || !spotlightTarget) return;
|
||||
const r = spotlightTarget.getBoundingClientRect();
|
||||
// Match the overlay's outer edge: element rect + 4px (2px overlay offset + 2px outline width)
|
||||
const inset = 4;
|
||||
const radius = 6; // outline border-radius (4) + outline width (2)
|
||||
// Match the overlay's outer edge: element rect + 3px (2px overlay offset + 1px hairline)
|
||||
const inset = 3;
|
||||
const radius = 5; // outline border-radius (4) + hairline width (1)
|
||||
const x1 = r.left - inset;
|
||||
const y1 = r.top - inset;
|
||||
const x2 = r.right + inset;
|
||||
@@ -3067,11 +3089,12 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
position: 'absolute', bottom: '100%', left: '-2px',
|
||||
display: 'flex', alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em',
|
||||
fontSize: '10px', fontWeight: '600', letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase',
|
||||
color: LABEL_INK, lineHeight: '14px',
|
||||
background: LABEL_BG,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
borderRadius: '4px 4px 0 0',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
||||
borderRadius: '3px 3px 0 0',
|
||||
});
|
||||
|
||||
const textSpan = document.createElement('span');
|
||||
@@ -3095,9 +3118,9 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
cycleMode = true;
|
||||
|
||||
const btnStyle = {
|
||||
background: 'none', border: 'none', color: 'rgba(255,255,255,0.7)',
|
||||
background: 'none', border: 'none', color: 'oklch(14% 0.018 95 / 0.7)',
|
||||
fontSize: '11px', cursor: 'pointer', padding: '3px 4px',
|
||||
fontFamily: 'system-ui, sans-serif', lineHeight: '14px',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', lineHeight: '14px',
|
||||
pointerEvents: 'auto',
|
||||
};
|
||||
|
||||
@@ -3188,8 +3211,10 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
banner.className = 'impeccable-overlay impeccable-banner';
|
||||
Object.assign(banner.style, {
|
||||
position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000',
|
||||
background: LABEL_BG, color: LABEL_INK,
|
||||
fontFamily: 'system-ui, sans-serif', fontSize: '13px',
|
||||
background: PAPER, color: PAPER_INK,
|
||||
borderBottom: `1px solid ${GOLD_LINE}`,
|
||||
boxShadow: BANNER_LIFT,
|
||||
fontFamily: SANS, fontSize: '12px',
|
||||
display: 'flex', alignItems: 'center', pointerEvents: 'auto',
|
||||
height: '36px', overflow: 'hidden', maxWidth: '100vw',
|
||||
transform: 'translateY(-100%)',
|
||||
@@ -3199,24 +3224,52 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
banner.style.transform = 'translateY(0)';
|
||||
}));
|
||||
|
||||
// The detector's indicator, the same lit gold dot the panel puts on a
|
||||
// section head.
|
||||
const dot = document.createElement('span');
|
||||
Object.assign(dot.style, {
|
||||
flexShrink: '0', width: '7px', height: '7px', margin: '0 0 0 12px',
|
||||
borderRadius: '50%', background: BRAND_COLOR,
|
||||
boxShadow: '0 0 0 1px oklch(13% 0 0 / 0.12), 0 0 4px oklch(84% 0.19 80 / 0.6)',
|
||||
});
|
||||
banner.appendChild(dot);
|
||||
|
||||
// Scrollable findings area
|
||||
const scrollArea = document.createElement('div');
|
||||
Object.assign(scrollArea.style, {
|
||||
flex: '1', minWidth: '0', overflowX: 'auto', overflowY: 'hidden',
|
||||
display: 'flex', gap: '8px', alignItems: 'center',
|
||||
display: 'flex', gap: '12px', alignItems: 'center',
|
||||
padding: '0 12px', scrollSnapType: 'x mandatory',
|
||||
scrollbarWidth: 'none',
|
||||
});
|
||||
for (const f of findings) {
|
||||
const prefix = RULE_CATEGORY[f.type] === 'slop' ? '\u2726 ' : '';
|
||||
const tag = document.createElement('span');
|
||||
tag.textContent = `${prefix}${TYPE_LABELS[f.type] || f.type}: ${f.detail}`;
|
||||
Object.assign(tag.style, {
|
||||
background: 'rgba(255,255,255,0.15)', padding: '2px 8px',
|
||||
borderRadius: '3px', fontSize: '12px', fontFamily: 'ui-monospace, monospace',
|
||||
const isSlop = RULE_CATEGORY[f.type] === 'slop';
|
||||
const item = document.createElement('span');
|
||||
Object.assign(item.style, {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
||||
whiteSpace: 'nowrap', flexShrink: '0', scrollSnapAlign: 'start',
|
||||
});
|
||||
scrollArea.appendChild(tag);
|
||||
|
||||
const tag = document.createElement('span');
|
||||
tag.textContent = TYPE_LABELS[f.type] || f.type;
|
||||
Object.assign(tag.style, {
|
||||
display: 'inline-flex', alignItems: 'center', minHeight: '16px',
|
||||
padding: '0 6px', borderRadius: '3px',
|
||||
background: isSlop ? BRAND_COLOR : PAPER_GRAY,
|
||||
color: isSlop ? LABEL_INK : PAPER_MUTED,
|
||||
fontFamily: MONO, fontSize: '10px', fontWeight: '600',
|
||||
letterSpacing: '0.06em', lineHeight: '1', textTransform: 'uppercase',
|
||||
});
|
||||
|
||||
const detail = document.createElement('span');
|
||||
detail.textContent = f.detail;
|
||||
Object.assign(detail.style, {
|
||||
color: PAPER_INK, fontFamily: SANS, fontSize: '12px',
|
||||
});
|
||||
|
||||
item.appendChild(tag);
|
||||
item.appendChild(detail);
|
||||
scrollArea.appendChild(item);
|
||||
}
|
||||
banner.appendChild(scrollArea);
|
||||
|
||||
@@ -3234,7 +3287,7 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
toggle.title = 'Toggle overlay visibility';
|
||||
Object.assign(toggle.style, {
|
||||
background: 'none', border: 'none',
|
||||
color: 'white', fontSize: '16px', cursor: 'pointer', padding: '0 4px',
|
||||
color: PAPER_MUTED, fontSize: '16px', cursor: 'pointer', padding: '0 4px',
|
||||
opacity: '0.85', transition: 'opacity 0.15s',
|
||||
});
|
||||
let overlaysVisible = true;
|
||||
@@ -3252,7 +3305,7 @@ function createImpeccableOverlay({ extensionMode = false, antipatterns = [] } =
|
||||
close.title = 'Dismiss banner';
|
||||
Object.assign(close.style, {
|
||||
background: 'none', border: 'none',
|
||||
color: 'white', fontSize: '18px', cursor: 'pointer', padding: '0 4px',
|
||||
color: PAPER_MUTED, fontSize: '18px', cursor: 'pointer', padding: '0 4px',
|
||||
});
|
||||
close.addEventListener('click', () => banner.remove());
|
||||
controls.appendChild(close);
|
||||
|
||||
@@ -31,7 +31,21 @@ function updateBadge(tabId) {
|
||||
const count = state?.findings?.reduce((sum, f) => sum + (f.findings?.length || 0), 0) || 0;
|
||||
const text = count > 0 ? String(count) : '';
|
||||
chrome.action.setBadgeText({ text, tabId }).catch(() => {});
|
||||
chrome.action.setBadgeBackgroundColor({ color: '#d6336c', tabId }).catch(() => {});
|
||||
// The detector's own tag: kinpaku gold (--ks-kinpaku, oklch(84% 0.19 80.46))
|
||||
// carrying dark ink (--ks-on-gold), the same pair the page overlay's label
|
||||
// and the panel's count use. Chrome's default badge text is white, which
|
||||
// does not clear 4.5:1 on gold; the ink pair measures about 11.8:1.
|
||||
chrome.action.setBadgeBackgroundColor({ color: '#ffba00', tabId }).catch(() => {});
|
||||
// setBadgeTextColor landed in Chrome 110 and is missing on older Chromium
|
||||
// and on Firefox's action API, so both the method and its return value are
|
||||
// checked: an optional call on a missing method returns undefined, and
|
||||
// .catch on undefined raises a TypeError that would escape updateBadge
|
||||
// into whatever asked for the update. The gold badge reads fine without
|
||||
// this call; all it fixes is Chrome's default white badge text.
|
||||
if (typeof chrome.action.setBadgeTextColor === 'function') {
|
||||
const pending = chrome.action.setBadgeTextColor({ color: '#0b0903', tabId });
|
||||
if (pending && typeof pending.catch === 'function') pending.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function notifyPanels(tabId, message) {
|
||||
|
||||
+382
-250
@@ -1,52 +1,41 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Light theme (default DevTools) */
|
||||
:root {
|
||||
--bg: #fff;
|
||||
--bg-subtle: #f5f5f5;
|
||||
--bg-hover: #eee;
|
||||
--text: #1a1a1a;
|
||||
--text-dim: #666;
|
||||
--accent: oklch(48% 0.25 350);
|
||||
--accent-dim: oklch(40% 0.18 350);
|
||||
--border: #ddd;
|
||||
--radius: 6px;
|
||||
}
|
||||
|
||||
/* Dark theme (set via JS from chrome.devtools.panels.themeName) */
|
||||
.theme-dark {
|
||||
--bg: #1a1a1a;
|
||||
--bg-subtle: #242424;
|
||||
--bg-hover: #2a2a2a;
|
||||
--text: #f5f3ef;
|
||||
--text-dim: #999;
|
||||
--accent: oklch(55% 0.25 350);
|
||||
--accent-dim: oklch(45% 0.18 350);
|
||||
--border: #333;
|
||||
}
|
||||
/*
|
||||
* DevTools panel: a paper sheet laid on the tool.
|
||||
*
|
||||
* Tokens, the severity language and the theme decision live in
|
||||
* ../shared/kinpaku.css. The short version, because it shapes every rule
|
||||
* below: one light theme in both DevTools themes; gold is the mark, the
|
||||
* tag and the lit dot on a control; patina carries any colored text;
|
||||
* selectors and counts are mono, as they are in the agent.
|
||||
*
|
||||
* How a finding reads here:
|
||||
* AI tells gold. A lit gold dot on the section, the count as the
|
||||
* detector's tag, the section name in ink.
|
||||
* Quality patina. A patina dot, a quiet gray count, a muted name.
|
||||
* Selector patina-deep mono in both sections: it is the one piece of
|
||||
* colored text, and patina is the color that may carry text.
|
||||
*
|
||||
* Controls are paper hardware: a recessed track, a raised cap that stands
|
||||
* a millimetre proud, and a gold dot that lights when the cap is the one
|
||||
* selected.
|
||||
*/
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-y: auto;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
font-size: var(--ks-type-label);
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
/* ============================================================
|
||||
Toolbar
|
||||
============================================================ */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
gap: 12px;
|
||||
padding: 7px 10px;
|
||||
background: var(--ks-paper);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
@@ -56,97 +45,199 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
opacity: 0.7;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 13px;
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-size: var(--ks-type-ui);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
letter-spacing: 0.16em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
/* The total, as the detector's own tag. */
|
||||
.badge {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
align-items: center;
|
||||
min-height: 16px;
|
||||
padding: 0 6px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-on-gold);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.badge.visible {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Tool buttons */
|
||||
/* Tool buttons: a cap that rises under the pointer. */
|
||||
.tool-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
color: var(--ks-text-muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
transition:
|
||||
background-color var(--ks-quick) var(--ks-ease),
|
||||
border-color var(--ks-quick) var(--ks-ease),
|
||||
color var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
background: var(--ks-paper-raised);
|
||||
border-color: var(--ks-rule);
|
||||
box-shadow: var(--ks-cap-lift);
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
.tool-btn:active {
|
||||
box-shadow: var(--ks-cap-press);
|
||||
}
|
||||
|
||||
/* Held down: a raised cap with its indicator lit. */
|
||||
.tool-btn.active {
|
||||
background: var(--ks-paper-raised);
|
||||
border-color: var(--ks-rule);
|
||||
box-shadow: var(--ks-cap-lift);
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
.tool-btn.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--ks-kinpaku);
|
||||
box-shadow: var(--ks-led);
|
||||
}
|
||||
|
||||
/* Overlays hidden: the eye reads off. */
|
||||
.tool-btn.inactive {
|
||||
opacity: 0.4;
|
||||
color: var(--ks-text-mute-deep);
|
||||
}
|
||||
|
||||
/* Findings */
|
||||
/* Copied: a state, so patina. */
|
||||
.tool-btn.copied {
|
||||
color: var(--ks-patina-deep);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Findings
|
||||
============================================================ */
|
||||
#findings-container {
|
||||
padding: 8px;
|
||||
padding: 6px 8px 16px;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 6px 4px;
|
||||
}
|
||||
|
||||
.category-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* AI tells: the lit gold dot. Quality: patina. */
|
||||
.category-dot-slop {
|
||||
background: var(--ks-kinpaku);
|
||||
box-shadow: var(--ks-led);
|
||||
}
|
||||
|
||||
.category-dot-quality {
|
||||
background: var(--ks-patina);
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: var(--ks-type-micro);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.category-slop .category-name {
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
/* The count is the tag in an AI-tell section, a quiet chip in a quality one. */
|
||||
.category-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 15px;
|
||||
padding: 0 5px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: var(--ks-gray);
|
||||
color: var(--ks-text-muted);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.category-slop .category-count {
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-on-gold);
|
||||
}
|
||||
|
||||
.finding-group {
|
||||
margin-bottom: 2px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius);
|
||||
padding: 5px 6px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s;
|
||||
transition: background-color var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.group-header:hover {
|
||||
background: var(--bg-hover);
|
||||
background: var(--ks-paper-deep);
|
||||
}
|
||||
|
||||
.group-chevron {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
transition: transform 0.15s;
|
||||
width: 12px;
|
||||
flex-shrink: 0;
|
||||
font-size: 9px;
|
||||
color: var(--ks-text-faint);
|
||||
transition: transform var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.group-header.collapsed .group-chevron {
|
||||
@@ -154,16 +245,17 @@ h1 {
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: var(--ks-type-label);
|
||||
font-weight: 600;
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
.group-count {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 10px;
|
||||
color: var(--ks-text-faint);
|
||||
}
|
||||
|
||||
.group-items {
|
||||
@@ -177,15 +269,15 @@ h1 {
|
||||
.finding-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 5px 8px 5px 28px;
|
||||
border-radius: var(--radius);
|
||||
gap: 1px;
|
||||
padding: 4px 6px 5px 26px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
transition: background-color var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.finding-item:hover {
|
||||
background: var(--bg-hover);
|
||||
background: var(--ks-paper-deep);
|
||||
}
|
||||
|
||||
.finding-row {
|
||||
@@ -200,6 +292,36 @@ h1 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* The one colored text in the list. Patina clears 4.5:1 on paper. */
|
||||
.finding-selector {
|
||||
font-family: var(--ks-mono);
|
||||
font-size: var(--ks-type-micro);
|
||||
color: var(--ks-patina-deep);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.finding-detail {
|
||||
font-size: var(--ks-type-micro);
|
||||
color: var(--ks-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.finding-description {
|
||||
display: none;
|
||||
padding: 3px 0 2px;
|
||||
font-size: var(--ks-type-micro);
|
||||
line-height: 1.45;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.finding-item:hover .finding-description {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.finding-copy {
|
||||
display: none;
|
||||
align-items: center;
|
||||
@@ -207,12 +329,12 @@ h1 {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
color: var(--ks-text-faint);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
transition: background-color var(--ks-quick) var(--ks-ease), color var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.finding-item:hover .finding-copy {
|
||||
@@ -220,72 +342,45 @@ h1 {
|
||||
}
|
||||
|
||||
.finding-copy:hover {
|
||||
background: var(--bg);
|
||||
color: var(--accent);
|
||||
background: var(--ks-paper-raised);
|
||||
border-color: var(--ks-rule);
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
.finding-copy.copied {
|
||||
color: var(--accent);
|
||||
color: var(--ks-patina-deep);
|
||||
}
|
||||
|
||||
.tool-btn.copied {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.finding-selector {
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--accent);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.finding-detail {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.finding-description {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
opacity: 0.7;
|
||||
line-height: 1.4;
|
||||
padding: 2px 0 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.finding-item:hover .finding-description {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Finding tags (page-level, hidden, etc.) */
|
||||
/* Tags on a finding: quiet by default. Gold is reserved for the section
|
||||
and the total, so a row-level tag never competes with it. */
|
||||
.finding-tag {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
min-height: 14px;
|
||||
margin-bottom: 2px;
|
||||
padding: 0 5px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tag-page {
|
||||
color: var(--accent-dim);
|
||||
background: transparent;
|
||||
background: var(--ks-patina-pale);
|
||||
color: var(--ks-patina-ink);
|
||||
}
|
||||
|
||||
.tag-hidden {
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-hover);
|
||||
background: var(--ks-gray);
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.finding-item.is-hidden {
|
||||
opacity: 0.55;
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -293,152 +388,157 @@ h1 {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
/* ============================================================
|
||||
Empty state
|
||||
============================================================ */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 24px;
|
||||
padding: 56px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
opacity: 0.3;
|
||||
margin-bottom: 12px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: var(--ks-gray);
|
||||
color: var(--ks-text-mute-deep);
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 3px;
|
||||
font-size: var(--ks-type-ui);
|
||||
font-weight: 600;
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
font-size: var(--ks-type-micro);
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
/* Category sections */
|
||||
.category-section {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 8px 4px;
|
||||
}
|
||||
|
||||
.category-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-dot-slop {
|
||||
background: oklch(55% 0.25 350);
|
||||
}
|
||||
|
||||
.category-dot-quality {
|
||||
background: var(--text-dim);
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
/* ============================================================
|
||||
Settings. Every control here is paper hardware.
|
||||
============================================================ */
|
||||
#settings-container {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 8px 8px;
|
||||
padding: 0 8px 10px;
|
||||
background: var(--ks-paper-deep);
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
font-size: 11px;
|
||||
padding: 10px 6px 6px;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: var(--ks-type-micro);
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 8px 8px 6px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
#settings-list .settings-header {
|
||||
padding: 12px 6px 4px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1px 12px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
#settings-list .settings-header {
|
||||
padding: 8px 8px 4px;
|
||||
gap: 0 12px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 8px 8px;
|
||||
gap: 12px;
|
||||
padding: 4px 6px 8px;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 11px;
|
||||
color: var(--text);
|
||||
font-size: var(--ks-type-label);
|
||||
color: var(--ks-text);
|
||||
}
|
||||
|
||||
/* Segmented control: a recessed track carrying raised caps. */
|
||||
.setting-segmented {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: var(--ks-gray);
|
||||
box-shadow: var(--ks-track-recess);
|
||||
}
|
||||
|
||||
.setting-segmented button {
|
||||
background: transparent;
|
||||
position: relative;
|
||||
padding: 3px 9px 3px 16px;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
color: var(--ks-text-muted);
|
||||
font-family: inherit;
|
||||
border-right: 1px solid var(--border);
|
||||
font-size: var(--ks-type-micro);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--ks-quick) var(--ks-ease),
|
||||
color var(--ks-quick) var(--ks-ease),
|
||||
box-shadow var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.setting-segmented button:last-child {
|
||||
border-right: none;
|
||||
/* The key's indicator: dark at rest, lit gold on the selected cap. */
|
||||
.setting-segmented button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 6px;
|
||||
transform: translateY(-50%);
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--ks-gray-2);
|
||||
box-shadow: inset 0 1px 1px oklch(13% 0 0 / 0.18);
|
||||
transition: background-color var(--ks-quick) var(--ks-ease), box-shadow var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.setting-segmented button:hover {
|
||||
color: var(--text);
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
.setting-segmented button.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
background: var(--ks-paper-raised);
|
||||
box-shadow: var(--ks-cap-lift);
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
.setting-segmented button.active::before {
|
||||
background: var(--ks-kinpaku);
|
||||
box-shadow: var(--ks-led);
|
||||
}
|
||||
|
||||
.setting-segmented button:active {
|
||||
box-shadow: var(--ks-cap-press);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
/* Switch: a recessed track, a paper knob, a dot that lights when on. */
|
||||
.setting-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-switch input {
|
||||
@@ -450,10 +550,10 @@ h1 {
|
||||
|
||||
.setting-switch-track {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--border);
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s ease;
|
||||
inset: 0;
|
||||
border-radius: var(--ks-radius-pill);
|
||||
background: var(--ks-gray-2);
|
||||
box-shadow: var(--ks-track-recess);
|
||||
}
|
||||
|
||||
.setting-switch-track::before {
|
||||
@@ -461,60 +561,92 @@ h1 {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--bg);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.15s ease;
|
||||
background: var(--ks-paper-raised);
|
||||
box-shadow: var(--ks-cap-lift);
|
||||
transition: transform 220ms cubic-bezier(0.3, 0.7, 0.2, 1);
|
||||
}
|
||||
|
||||
.setting-switch input:checked + .setting-switch-track {
|
||||
background: var(--accent);
|
||||
.setting-switch-track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 7px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--ks-gray-2);
|
||||
box-shadow: inset 0 1px 1px oklch(13% 0 0 / 0.18);
|
||||
transition:
|
||||
transform 220ms cubic-bezier(0.3, 0.7, 0.2, 1),
|
||||
background-color var(--ks-quick) var(--ks-ease),
|
||||
box-shadow var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.setting-switch input:checked + .setting-switch-track::before {
|
||||
.setting-switch input:checked + .setting-switch-track::before,
|
||||
.setting-switch input:checked + .setting-switch-track::after {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
.setting-switch input:checked + .setting-switch-track::after {
|
||||
background: var(--ks-kinpaku);
|
||||
box-shadow: var(--ks-led);
|
||||
}
|
||||
|
||||
.setting-switch input:focus-visible + .setting-switch-track {
|
||||
outline: 2px solid var(--ks-patina);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Rule toggles */
|
||||
.setting-rule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 11px;
|
||||
gap: 7px;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
font-size: var(--ks-type-micro);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
transition: background-color var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.setting-rule:hover {
|
||||
background: var(--bg-hover);
|
||||
background: var(--ks-gray);
|
||||
}
|
||||
|
||||
.setting-rule input[type="checkbox"] {
|
||||
margin: 0;
|
||||
accent-color: var(--accent);
|
||||
accent-color: var(--ks-patina-deep);
|
||||
}
|
||||
|
||||
/* Scanning state */
|
||||
/* ============================================================
|
||||
Scanning
|
||||
============================================================ */
|
||||
.scanning-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
padding: 14px 12px;
|
||||
font-size: var(--ks-type-label);
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.scanning-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
background: var(--ks-kinpaku);
|
||||
box-shadow: var(--ks-led);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
0%, 100% { opacity: 0.35; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.scanning-dot { animation: none; }
|
||||
}
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../shared/kinpaku.css">
|
||||
<link rel="stylesheet" href="panel.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="logo">/</span>
|
||||
<span class="ks-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 32 32" fill="currentColor">
|
||||
<path d="M7 1 L18 1 L8 31 L7 31 Q1 31 1 25 L1 7 Q1 1 7 1 Z"/>
|
||||
<path d="M22 1 L25 1 Q31 1 31 7 L31 25 Q31 31 25 31 L12 31 Z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<h1>Impeccable</h1>
|
||||
<span class="badge" id="badge">0</span>
|
||||
</div>
|
||||
|
||||
@@ -1,97 +1,94 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #fff;
|
||||
--text: #1a1a1a;
|
||||
--text-dim: #666;
|
||||
--text-faint: #999;
|
||||
--accent: oklch(48% 0.25 350);
|
||||
--rule: #e8e6e2;
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
--bg: #1a1a1a;
|
||||
--text: #f5f3ef;
|
||||
--text-dim: #9a9590;
|
||||
--text-faint: #666;
|
||||
--accent: oklch(60% 0.25 350);
|
||||
--rule: #2a2a2a;
|
||||
}
|
||||
/*
|
||||
* Elements sidebar: the findings on the selected element, on paper.
|
||||
*
|
||||
* Tokens and the severity language are in ../shared/kinpaku.css. The pane is
|
||||
* narrow, so the language is carried by two small pieces: the kind label
|
||||
* (the detector's gold tag for an AI tell, a quiet gray chip for a quality
|
||||
* issue) and the detail line in patina-deep mono.
|
||||
*/
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 12px 14px;
|
||||
padding: 10px 12px 14px;
|
||||
font-size: var(--ks-type-label);
|
||||
}
|
||||
|
||||
/* Empty / no-findings states */
|
||||
/* Empty and no-findings states */
|
||||
|
||||
.state {
|
||||
color: var(--text-faint);
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
padding: 4px 0;
|
||||
padding: 3px 0;
|
||||
font-size: var(--ks-type-label);
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
.state strong {
|
||||
color: var(--text-dim);
|
||||
font-style: normal;
|
||||
color: var(--ks-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Finding list */
|
||||
/* Findings */
|
||||
|
||||
.finding + .finding {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--rule);
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
.finding-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.finding-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: var(--ks-type-label);
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.005em;
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
/* The slop marker: gold, and the only gold glyph in the pane. */
|
||||
.finding-name .marker {
|
||||
color: var(--accent);
|
||||
margin-right: 4px;
|
||||
color: var(--ks-kinpaku-rich);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* AI tell: the detector's tag. Quality: a quiet chip. */
|
||||
.finding-kind {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 15px;
|
||||
padding: 0 5px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: var(--ks-gray);
|
||||
color: var(--ks-text-muted);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-faint);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.finding-kind.is-slop {
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-on-gold);
|
||||
}
|
||||
|
||||
.finding-detail {
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 5px;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: var(--ks-type-micro);
|
||||
color: var(--ks-patina-deep);
|
||||
}
|
||||
|
||||
.finding-description {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.55;
|
||||
font-size: var(--ks-type-micro);
|
||||
line-height: 1.5;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../shared/kinpaku.css">
|
||||
<link rel="stylesheet" href="sidebar.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -85,7 +85,7 @@ function render(items) {
|
||||
<div class="finding">
|
||||
<div class="finding-header">
|
||||
<span class="finding-name">${marker}${escapeHtml(f.name)}</span>
|
||||
<span class="finding-kind">${kind}</span>
|
||||
<span class="finding-kind ${isSlop ? 'is-slop' : 'is-quality'}">${kind}</span>
|
||||
</div>
|
||||
<div class="finding-detail">${escapeHtml(f.detail)}</div>
|
||||
<div class="finding-description">${escapeHtml(f.description)}</div>
|
||||
|
||||
+119
-137
@@ -1,193 +1,175 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--ks-kinpaku: oklch(84% 0.19 80.46);
|
||||
--ks-kinpaku-pale: oklch(86% 0.07 84);
|
||||
--ks-patina: oklch(70% 0.12 188);
|
||||
--ks-lacquer: oklch(7% 0.006 95);
|
||||
--ks-lacquer-deep: oklch(4% 0.004 95);
|
||||
--ks-text: oklch(88% 0 0);
|
||||
--ks-text-muted: oklch(72% 0 0);
|
||||
--ks-text-faint: oklch(62% 0 0);
|
||||
--ks-rule: oklch(78% 0 0 / 0.16);
|
||||
--popup-brand: var(--ks-kinpaku);
|
||||
--ks-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
/*
|
||||
* Popup: the toolbar sheet. Paper ground, the mark and the wordmark at the
|
||||
* head, one count, two controls. Tokens and the severity language live in
|
||||
* ../shared/kinpaku.css; only layout and the local pieces are here.
|
||||
*
|
||||
* The count is ink at every value: gold under 2:1 on paper cannot carry a
|
||||
* numeral. What findings change is the rule under it, which lights gold.
|
||||
* Neither button carries the kit's gold arrow, because popup.js rewrites
|
||||
* both labels through textContent and would drop any child element.
|
||||
*/
|
||||
|
||||
body {
|
||||
width: 220px;
|
||||
background: var(--ks-lacquer);
|
||||
color: var(--ks-text);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 13px;
|
||||
padding: 16px;
|
||||
width: 224px;
|
||||
padding: 14px 14px 10px;
|
||||
}
|
||||
|
||||
/* Head */
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--popup-brand);
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: block;
|
||||
gap: 8px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--popup-brand);
|
||||
font-size: 12px;
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-size: var(--ks-type-ui);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.15em;
|
||||
letter-spacing: 0.16em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
/* Count */
|
||||
|
||||
.count-display {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
padding: 18px 0 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.count-number {
|
||||
display: block;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
display: inline-block;
|
||||
min-width: 56px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 2px solid var(--ks-rule);
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-size: 44px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
color: var(--ks-text-faint);
|
||||
transition: color 0.2s;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--ks-text-mute-deep);
|
||||
transition: color var(--ks-settle) var(--ks-ease), border-color var(--ks-settle) var(--ks-ease);
|
||||
}
|
||||
|
||||
/* Findings present: the numeral goes ink and the rule under it lights gold. */
|
||||
.count-number.has-findings {
|
||||
color: var(--ks-kinpaku);
|
||||
color: var(--ks-ink);
|
||||
border-bottom-color: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: var(--ks-type-micro);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
font-family: inherit;
|
||||
font-size: var(--ks-type-label);
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--ks-quick) var(--ks-ease),
|
||||
border-color var(--ks-quick) var(--ks-ease),
|
||||
color var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ks-ink);
|
||||
border-color: var(--ks-ink);
|
||||
color: var(--ks-paper-raised);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--ks-text);
|
||||
border-color: var(--ks-text);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
background: oklch(8% 0 0);
|
||||
border-color: oklch(8% 0 0);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: transparent;
|
||||
border-color: var(--ks-rule);
|
||||
color: var(--ks-text-mute-deep);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--ks-paper-raised);
|
||||
border-color: var(--ks-edge);
|
||||
color: var(--ks-ink);
|
||||
box-shadow: var(--ks-cap-lift);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--ks-ink);
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: var(--ks-gray);
|
||||
box-shadow: var(--ks-cap-press);
|
||||
}
|
||||
|
||||
/* A scan that did not run. The one vermilion in the extension. */
|
||||
|
||||
.scan-error {
|
||||
margin: -8px 0 16px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: oklch(70% 0.13 30);
|
||||
margin: -8px 0 14px;
|
||||
padding-left: 8px;
|
||||
border-left: 2px solid var(--ks-vermilion);
|
||||
font-size: var(--ks-type-micro);
|
||||
line-height: 1.45;
|
||||
color: var(--ks-vermilion);
|
||||
}
|
||||
|
||||
.scan-error[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 180ms var(--ks-ease),
|
||||
background-color 180ms var(--ks-ease),
|
||||
border-color 180ms var(--ks-ease),
|
||||
color 180ms var(--ks-ease),
|
||||
opacity 0.15s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ks-kinpaku);
|
||||
border-color: var(--ks-kinpaku);
|
||||
color: var(--ks-lacquer-deep);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--ks-kinpaku-pale);
|
||||
border-color: var(--ks-kinpaku-pale);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
border-color: var(--ks-kinpaku);
|
||||
color: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: oklch(77% 0.14 82 / 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid var(--ks-patina);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
/* Foot */
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding-top: 8px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--ks-rule);
|
||||
}
|
||||
|
||||
footer a {
|
||||
font-size: 11px;
|
||||
font-size: var(--ks-type-micro);
|
||||
color: var(--ks-text-muted);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
padding-bottom: 1px;
|
||||
transition: color var(--ks-quick) var(--ks-ease), border-color var(--ks-quick) var(--ks-ease);
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--popup-brand);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ks-kinpaku-deep: oklch(61% 0.085 78);
|
||||
--ks-lacquer: oklch(97% 0.012 95);
|
||||
--ks-text: oklch(25% 0.018 95);
|
||||
--ks-text-muted: oklch(45% 0.015 95);
|
||||
--ks-text-faint: oklch(55% 0.012 95);
|
||||
--ks-rule: oklch(25% 0.02 95 / 0.12);
|
||||
--popup-brand: oklch(18% 0.02 95);
|
||||
}
|
||||
|
||||
.count-number.has-findings {
|
||||
color: var(--ks-kinpaku-deep);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: oklch(14% 0.018 95);
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--ks-kinpaku-deep);
|
||||
color: var(--ks-kinpaku-deep);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: oklch(77% 0.13 82 / 0.10);
|
||||
}
|
||||
color: var(--ks-ink);
|
||||
border-bottom-color: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../shared/kinpaku.css">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<span class="logo" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"/>
|
||||
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"/>
|
||||
<span class="ks-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 32 32" fill="currentColor">
|
||||
<path d="M7 1 L18 1 L8 31 L7 31 Q1 31 1 25 L1 7 Q1 1 7 1 Z"/>
|
||||
<path d="M22 1 L25 1 Q31 1 31 7 L31 25 Q31 31 25 31 L12 31 Z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<h1>Impeccable</h1>
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Impeccable extension: the shared surface.
|
||||
*
|
||||
* One system across the popup, the DevTools panel and the Elements sidebar,
|
||||
* mirroring impeccable.style's "paper and instruments" tokens
|
||||
* (site/styles/kinpaku-tokens.css). Values are copied, never invented; when
|
||||
* the site's tokens move, move these with them.
|
||||
*
|
||||
* The rules that shape everything below:
|
||||
*
|
||||
* Paper and ink. Surfaces are neutral paper (chroma 0), type is ink.
|
||||
* No warm cast, no dark section backgrounds.
|
||||
* Gold is jewelry. Kinpaku fills the mark, a hairline, the lit dot on a
|
||||
* control, and the tag. It never carries text on paper
|
||||
* and never fills a panel.
|
||||
* Patina reads. When color has to be read as text it is patina-deep,
|
||||
* which clears 4.5:1 on paper. Gold at 84% lightness
|
||||
* lands under 2:1 there, so gold text is a bug.
|
||||
* Instruments. A dark surface is something you operate. In here that
|
||||
* is a segmented control and a switch: a recessed track,
|
||||
* a raised cap, a lit gold dot.
|
||||
*
|
||||
* Severity language (the one mapping this UI uses, everywhere):
|
||||
*
|
||||
* AI tell the detector's own gold tag, and a lit gold dot on the
|
||||
* category. It is the finding the product is named for, so it
|
||||
* gets the brand's one loud mark.
|
||||
* Quality patina: a patina dot and patina-deep text. Present, read as
|
||||
* state, never competing with the gold.
|
||||
* Failure vermilion, and only for a scan that did not run.
|
||||
*
|
||||
* Theme. The brand has one light theme, so this has one too, in the popup
|
||||
* and in both DevTools themes. A paper panel inside dark DevTools is a
|
||||
* deliberate call: the panel is a sheet laid on the tool, so it carries an
|
||||
* ink toolbar seam and a hard outer edge to sit against the dark frame
|
||||
* rather than bleed into it. `.theme-dark` (set by panel.js and sidebar.js
|
||||
* from chrome.devtools.panels.themeName) tunes that seam; it does not
|
||||
* invert the surface.
|
||||
*
|
||||
* Fonts. Extension pages cannot load web fonts, so the two site faces are
|
||||
* matched from system stacks: Avenir Next (with Segoe UI Variable and
|
||||
* system-ui behind it) stands in for Albert Sans, the geometric humanist
|
||||
* that carries everything read; Avenir Next Condensed (Roboto Condensed,
|
||||
* Arial Narrow) stands in for Alumni Sans in the wordmark; ui-monospace
|
||||
* carries keys, selectors and the tag, as JetBrains Mono does on the site.
|
||||
*/
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/* Gold. The mark, a line, a lit dot, the tag. Never text on paper. */
|
||||
--ks-kinpaku: oklch(84% 0.19 80.46);
|
||||
--ks-kinpaku-vivid: oklch(87% 0.20 85);
|
||||
--ks-kinpaku-rich: oklch(77% 0.13 82);
|
||||
--ks-kinpaku-deep: oklch(61% 0.085 78);
|
||||
--ks-on-gold: oklch(14% 0.018 95);
|
||||
|
||||
/* Patina. State, links, selection, and any text that has to be colored. */
|
||||
--ks-patina: oklch(70% 0.12 188);
|
||||
--ks-patina-pale: oklch(82% 0.07 188);
|
||||
--ks-patina-deep: oklch(46% 0.08 188);
|
||||
--ks-patina-ink: oklch(38% 0.08 188);
|
||||
|
||||
/* Failure only. */
|
||||
--ks-vermilion: oklch(52% 0.16 35);
|
||||
|
||||
/* Paper. */
|
||||
--ks-paper: oklch(97.8% 0 0);
|
||||
--ks-paper-raised: oklch(99.5% 0 0);
|
||||
--ks-paper-deep: oklch(95% 0 0);
|
||||
--ks-gray: oklch(92% 0 0);
|
||||
--ks-gray-2: oklch(88% 0 0);
|
||||
|
||||
/* Ink. */
|
||||
--ks-ink: oklch(13% 0 0);
|
||||
--ks-text: oklch(22% 0 0);
|
||||
--ks-text-muted: oklch(46% 0 0);
|
||||
--ks-text-faint: oklch(56% 0 0);
|
||||
--ks-text-mute-deep: oklch(66% 0 0);
|
||||
|
||||
/* Rules. A divider is faint; the bound of a control clears 3:1. */
|
||||
--ks-rule: oklch(13% 0 0 / 0.08);
|
||||
--ks-edge: oklch(13% 0 0 / 0.45);
|
||||
|
||||
/* Shape. */
|
||||
--ks-radius-sm: 3px;
|
||||
--ks-radius-md: 8px;
|
||||
--ks-radius-pill: 999px;
|
||||
|
||||
/* Paper hardware. A cap stands proud of a recessed track; the dot lights. */
|
||||
--ks-cap-lift: inset 0 1px 0 oklch(100% 0 0 / 0.9), 0 1px 0 oklch(13% 0 0 / 0.14), 0 2px 3px oklch(13% 0 0 / 0.08);
|
||||
--ks-cap-press: inset 0 1px 2px oklch(13% 0 0 / 0.14);
|
||||
--ks-track-recess: inset 0 1px 3px oklch(13% 0 0 / 0.16), inset 0 -1px 0 oklch(100% 0 0 / 0.7);
|
||||
--ks-led: 0 0 0 1px oklch(13% 0 0 / 0.12), 0 0 4px oklch(84% 0.19 80 / 0.6);
|
||||
--ks-lift-1: 0 1px 1px oklch(13% 0 0 / 0.05), 0 2px 3px oklch(13% 0 0 / 0.04), 0 6px 12px oklch(13% 0 0 / 0.05);
|
||||
|
||||
/* Motion. */
|
||||
--ks-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
--ks-quick: 120ms;
|
||||
--ks-settle: 200ms;
|
||||
|
||||
/* Type. System stands-in for the two site faces; see the header. */
|
||||
--ks-font: "Avenir Next", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
|
||||
--ks-font-wordmark: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", var(--ks-font);
|
||||
--ks-mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
|
||||
/* Dense UI ramp. 11px is the floor for anything functional. */
|
||||
--ks-type-micro: 11px;
|
||||
--ks-type-label: 12px;
|
||||
--ks-type-ui: 13px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--ks-paper);
|
||||
color: var(--ks-text);
|
||||
font-family: var(--ks-font);
|
||||
font-size: var(--ks-type-label);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
The mark. The one place gold fills a shape: a carved ink tile
|
||||
split by the slash, the same glyph the toolbar icon carries.
|
||||
============================================================ */
|
||||
.ks-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: var(--ks-ink);
|
||||
color: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
.ks-mark svg {
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.ks-wordmark {
|
||||
font-family: var(--ks-font-wordmark);
|
||||
font-size: var(--ks-type-ui);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.16em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
color: var(--ks-ink);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
The tag. The detector's own label: small, mono, ink on gold.
|
||||
It is how a finding names itself in the page overlay, so it is
|
||||
how it names itself here.
|
||||
============================================================ */
|
||||
.ks-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-height: 16px;
|
||||
padding: 0 5px;
|
||||
border-radius: var(--ks-radius-sm);
|
||||
background: var(--ks-kinpaku);
|
||||
color: var(--ks-on-gold);
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ks-tag.is-quiet {
|
||||
background: var(--ks-gray);
|
||||
color: var(--ks-text-muted);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Focus. Patina, everywhere, at 2px.
|
||||
============================================================ */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--ks-patina);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Dark DevTools: the panel stays paper, and the seam hardens so the sheet
|
||||
reads as laid on the tool rather than bleeding into its frame. A hard top
|
||||
edge gives the sheet a boundary against the dark chrome above it, and the
|
||||
dividers inside step up so they still read next to that edge. */
|
||||
.theme-dark {
|
||||
--ks-rule: oklch(13% 0 0 / 0.12);
|
||||
}
|
||||
|
||||
/* The seam is drawn over the top of the sticky toolbar on purpose, which is
|
||||
the only way a 2px edge stays put while the sheet scrolls under it. It is
|
||||
paint and nothing else: pointer-events are off, so the toolbar's top row of
|
||||
pixels stays clickable. */
|
||||
.theme-dark body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: oklch(13% 0 0 / 0.55);
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export const SUITES = {
|
||||
description: 'Extension packaging checks (the rule logic itself is covered by the crate tests and the oracle).',
|
||||
triggers: [
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^extension\/(background|content|detector|devtools|offscreen|popup|manifest\.json)/,
|
||||
/^extension\/(background|content|detector|devtools|offscreen|popup|shared|manifest\.json)/,
|
||||
/^scripts\/build-extension\.js$/,
|
||||
/^browser-bundle\//,
|
||||
// Everything `cargo xtask bundle` reads: the rules and the registry
|
||||
|
||||
@@ -20,3 +20,37 @@ describe('extension DevTools packaging', () => {
|
||||
assert.doesNotMatch(source, /['"]icons\/icon-32\.png['"]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extension badge colors', () => {
|
||||
// Optional-call syntax on a method a browser does not implement returns
|
||||
// undefined, and .catch on undefined throws: updateBadge would abort before
|
||||
// it set the badge at all. Firefox's action API has no setBadgeTextColor,
|
||||
// so the call has to be guarded by an existence check, not by `?.`.
|
||||
it('guards setBadgeTextColor instead of chaining .catch onto an optional call', () => {
|
||||
const source = readFileSync(path.join(ROOT, 'extension/background/service-worker.js'), 'utf-8');
|
||||
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/setBadgeTextColor\?\.\([^)]*\)\s*\.catch/s,
|
||||
'setBadgeTextColor?.(...).catch(...) throws wherever the method is missing',
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/typeof chrome\.action\.setBadgeTextColor === 'function'/,
|
||||
'the call needs an existence check around it',
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/typeof pending\.catch === 'function'/,
|
||||
'the return value needs a promise check before .catch',
|
||||
);
|
||||
});
|
||||
|
||||
it('paints the badge in kinpaku gold with ink text', () => {
|
||||
const source = readFileSync(path.join(ROOT, 'extension/background/service-worker.js'), 'utf-8');
|
||||
|
||||
assert.match(source, /setBadgeBackgroundColor\(\{ color: '#ffba00'/);
|
||||
assert.match(source, /setBadgeTextColor\(\{ color: '#0b0903'/);
|
||||
assert.doesNotMatch(source, /#d6336c/, 'the magenta badge is retired');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user