From 9da21fc09ab7cf8760b82ec629544f616d0191e5 Mon Sep 17 00:00:00 2001
From: Adam
Date: Wed, 1 Jul 2026 03:17:34 +0200
Subject: [PATCH 1/5] refactor: remove dead code + accessibility & polish pass
Cleanup (grep-confirmed 0-usage, no behavior change):
- Remove parseShadow, detectTypeScaleRatio, parseComputedStyles,
getElementPath, ColorToken.rgb/hsl, OPEN_SIDE_PANEL enum, dead
GET/SET_PREFERENCES service-worker handlers; dedupe isPixelLensElement.
Accessibility & polish:
- focus-visible + aria-current on nav, aria-label on all icon-only buttons
- prefers-reduced-motion (CSS media + GSAP guard helper)
- text-dim contrast raised to AA (4.81:1)
- unified logo (PixelLensLogo), real keyboard shortcuts (manifest suggested_key + mac)
- brand accent left unchanged (user decision)
---
manifest.json | 10 +++-
src/background/service-worker.ts | 18 -------
src/content/scanner/ColorExtractor.ts | 4 +-
src/content/scanner/DesignSystemBuilder.ts | 38 ++-----------
src/content/scanner/TypographyExtractor.ts | 39 --------------
src/content/ui/ContentApp.tsx | 10 +---
src/content/ui/FloatingToolbar.tsx | 8 +++
src/lib/__tests__/colors.test.ts | 12 ++---
src/lib/__tests__/design-tokens.test.ts | 2 -
src/lib/__tests__/dom-utils.test.ts | 47 +---------------
src/lib/css-parser.ts | 62 ----------------------
src/lib/dom-utils.ts | 38 -------------
src/popup/Popup.tsx | 39 +++++++++++---
src/sidepanel/App.tsx | 20 ++++---
src/sidepanel/components/CSSBlock.tsx | 1 +
src/sidepanel/components/ColorSwatch.tsx | 6 ++-
src/sidepanel/components/ExportButton.tsx | 3 ++
src/sidepanel/components/PixelLensLogo.tsx | 40 ++++++++++++++
src/sidepanel/components/ShadowPreview.tsx | 1 +
src/sidepanel/reducedMotion.ts | 16 ++++++
src/sidepanel/styles/panel.css | 2 +-
src/sidepanel/views/CrawlView.tsx | 2 +
src/sidepanel/views/DesignSystemView.tsx | 1 +
src/sidepanel/views/ExportView.tsx | 1 +
src/sidepanel/views/MarkdownView.tsx | 2 +
src/sidepanel/views/ScanView.tsx | 7 +++
src/styles/globals.css | 18 ++++++-
src/types/design-system.ts | 4 +-
src/types/messages.ts | 7 ---
29 files changed, 169 insertions(+), 289 deletions(-)
create mode 100644 src/sidepanel/components/PixelLensLogo.tsx
create mode 100644 src/sidepanel/reducedMotion.ts
diff --git a/manifest.json b/manifest.json
index c9c1d35..9a140b7 100644
--- a/manifest.json
+++ b/manifest.json
@@ -39,10 +39,16 @@
}
},
"commands": {
- "_execute_action": {},
+ "_execute_action": {
+ "suggested_key": {
+ "default": "Ctrl+Shift+P",
+ "mac": "Command+Shift+P"
+ }
+ },
"toggle-inspect": {
"suggested_key": {
- "default": "Ctrl+Shift+L"
+ "default": "Ctrl+Shift+L",
+ "mac": "Command+Shift+L"
},
"description": "Toggle inspect mode"
}
diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts
index 16ac668..fb717a8 100644
--- a/src/background/service-worker.ts
+++ b/src/background/service-worker.ts
@@ -159,24 +159,6 @@ chrome.runtime.onMessage.addListener((message: IncomingMessage, sender, sendResp
})
return true // async response
}
-
- case MessageType.GET_PREFERENCES: {
- chrome.storage.sync.get('pixellens_preferences', (result) => {
- sendResponse(result['pixellens_preferences'] || {
- colorFormat: 'hex',
- gridSize: 8,
- theme: 'dark',
- })
- })
- return true // async response
- }
-
- case MessageType.SET_PREFERENCES: {
- const prefs = payload as MessagePayloadMap[MessageType.SET_PREFERENCES]
- chrome.storage.sync.set({ pixellens_preferences: prefs.preferences })
- sendResponse({ success: true })
- break
- }
}
})
diff --git a/src/content/scanner/ColorExtractor.ts b/src/content/scanner/ColorExtractor.ts
index 90fac81..132cfc1 100644
--- a/src/content/scanner/ColorExtractor.ts
+++ b/src/content/scanner/ColorExtractor.ts
@@ -1,6 +1,6 @@
// PixelLens — Color Extractor (extract and cluster colors from the page)
-import { toRgb, toHsl, isTransparent, clusterColors, classifyColors, tryToHex } from '@/lib/colors'
+import { isTransparent, clusterColors, classifyColors, tryToHex } from '@/lib/colors'
import type { ColorToken } from '@/types/design-system'
const COLOR_PROPS = ['color', 'background-color', 'border-color', 'outline-color'] as const
@@ -44,8 +44,6 @@ export class ColorExtractor {
const tokens: ColorToken[] = clustered.map((c) => ({
name: '',
hex: c.hex,
- rgb: toRgb(c.hex),
- hsl: toHsl(c.hex),
frequency: c.frequency,
category: 'accent' as const,
}))
diff --git a/src/content/scanner/DesignSystemBuilder.ts b/src/content/scanner/DesignSystemBuilder.ts
index 2daba9e..f2a6bcc 100644
--- a/src/content/scanner/DesignSystemBuilder.ts
+++ b/src/content/scanner/DesignSystemBuilder.ts
@@ -6,7 +6,6 @@ import type {
TypographyToken,
SpacingToken,
ShadowToken,
- ShadowParsed,
BorderRadiusToken,
} from '@/types/design-system'
@@ -46,44 +45,15 @@ export class DesignSystemBuilder {
// Deduplicate by raw value
if (shadowSet.has(shadow)) continue
- const parsed = this.parseShadow(shadow)
- if (parsed) {
- shadowSet.set(shadow, { value: shadow, parsed })
- }
+ // Store the raw computed value only. The previous parseShadow() output
+ // (ShadowToken.parsed) was never read by any consumer — ShadowPreview
+ // renders shadow.value directly — so the parse was dead + buggy work.
+ shadowSet.set(shadow, { value: shadow })
}
return Array.from(shadowSet.values())
}
- private parseShadow(shadow: string): ShadowParsed | null {
- // Basic shadow parsing:
- // Computed values always resolve to rgb() format
- const rgbMatch = shadow.match(/(rgba?\([^)]+\))\s+(-?[\d.]+px)\s+(-?[\d.]+px)\s+([\d.]+px)\s*([\d.]+px)?/)
- if (rgbMatch) {
- return {
- color: rgbMatch[1],
- x: rgbMatch[2],
- y: rgbMatch[3],
- blur: rgbMatch[4],
- spread: rgbMatch[5] || '0px',
- }
- }
-
- // Alternate order: offsets first then color
- const altMatch = shadow.match(/(-?[\d.]+px)\s+(-?[\d.]+px)\s+([\d.]+px)\s*([\d.]+px)?\s+(rgba?\([^)]+\))/)
- if (altMatch) {
- return {
- x: altMatch[1],
- y: altMatch[2],
- blur: altMatch[3],
- spread: altMatch[4] || '0px',
- color: altMatch[5],
- }
- }
-
- return null
- }
-
private extractBorderRadius(elements: Element[]): BorderRadiusToken[] {
const freqMap = new Map()
diff --git a/src/content/scanner/TypographyExtractor.ts b/src/content/scanner/TypographyExtractor.ts
index 4ae0ca2..2bc6ad0 100644
--- a/src/content/scanner/TypographyExtractor.ts
+++ b/src/content/scanner/TypographyExtractor.ts
@@ -8,8 +8,6 @@ const TEXT_TAGS = new Set([
'td', 'th', 'caption', 'blockquote',
])
-const KNOWN_RATIOS = [1.067, 1.125, 1.2, 1.25, 1.333, 1.414, 1.5, 1.618] as const
-
export class TypographyExtractor {
extract(elements: Element[]): TypographyToken[] {
// Family → Map<"size|weight" → variant with count>
@@ -70,41 +68,4 @@ export class TypographyExtractor {
return tokens
}
-
- detectTypeScaleRatio(tokens: TypographyToken[]): number | null {
- // Collect all unique font sizes across all families
- const sizes = new Set()
- for (const token of tokens) {
- for (const v of token.variants) {
- const px = parseFloat(v.fontSize)
- if (px > 0) sizes.add(px)
- }
- }
-
- const sorted = Array.from(sizes).sort((a, b) => a - b)
- if (sorted.length < 3) return null
-
- // Compute ratios between consecutive sizes
- const ratios: number[] = []
- for (let i = 1; i < sorted.length; i++) {
- ratios.push(sorted[i] / sorted[i - 1])
- }
-
- // Find the median ratio
- ratios.sort((a, b) => a - b)
- const median = ratios[Math.floor(ratios.length / 2)]
-
- // Match to nearest known ratio
- let closest: number = KNOWN_RATIOS[0]
- let minDiff = Math.abs(median - closest)
- for (const r of KNOWN_RATIOS) {
- const diff = Math.abs(median - r)
- if (diff < minDiff) {
- minDiff = diff
- closest = r
- }
- }
-
- return minDiff < 0.1 ? closest : null
- }
}
diff --git a/src/content/ui/ContentApp.tsx b/src/content/ui/ContentApp.tsx
index 1b187c8..77f1dbc 100644
--- a/src/content/ui/ContentApp.tsx
+++ b/src/content/ui/ContentApp.tsx
@@ -6,6 +6,7 @@ import { FloatingToolbar } from './FloatingToolbar'
import { InspectorTooltip } from './InspectorTooltip'
import { sendMessage } from '@/lib/messaging'
import { MessageType } from '@/types/messages'
+import { isPixelLensElement } from '@/lib/dom-utils'
type ContentMode = 'off' | 'inspect' | 'measure' | 'grid'
@@ -98,15 +99,6 @@ function ContentApp() {
)
}
-function isPixelLensElement(el: Element): boolean {
- let node: Node | null = el
- while (node) {
- if ((node as HTMLElement).id === 'pixellens-host') return true
- node = node.parentNode
- }
- return false
-}
-
export function mountContentApp(container: HTMLElement): void {
const root = createRoot(container)
root.render( )
diff --git a/src/content/ui/FloatingToolbar.tsx b/src/content/ui/FloatingToolbar.tsx
index c5cee9a..4711a5c 100644
--- a/src/content/ui/FloatingToolbar.tsx
+++ b/src/content/ui/FloatingToolbar.tsx
@@ -8,6 +8,7 @@ import {
Scan,
} from '@phosphor-icons/react'
import gsap from 'gsap'
+import { prefersReducedMotion } from '@/sidepanel/reducedMotion'
type ContentMode = 'off' | 'inspect' | 'measure' | 'grid'
@@ -44,6 +45,12 @@ export function FloatingToolbar({
const el = toolbarRef.current
if (!el) return
+ if (prefersReducedMotion()) {
+ // Appear in place, no slide-up.
+ gsap.set(el, { y: 0, opacity: 1 })
+ return
+ }
+
gsap.fromTo(
el,
{ y: 30, opacity: 0 },
@@ -159,6 +166,7 @@ export function FloatingToolbar({
{buttons.map((btn) => (
{
e.stopPropagation()
diff --git a/src/lib/__tests__/colors.test.ts b/src/lib/__tests__/colors.test.ts
index e0922a8..bad92ef 100644
--- a/src/lib/__tests__/colors.test.ts
+++ b/src/lib/__tests__/colors.test.ts
@@ -200,9 +200,9 @@ describe('clusterColors', () => {
describe('classifyColors', () => {
it('classifies chromatic colors as primary/secondary/accent', () => {
const colors: ColorToken[] = [
- { name: '', hex: '#ff0000', rgb: { r: 255, g: 0, b: 0 }, hsl: { h: 0, s: 100, l: 50 }, frequency: 10, category: 'primary' },
- { name: '', hex: '#00ff00', rgb: { r: 0, g: 255, b: 0 }, hsl: { h: 120, s: 100, l: 50 }, frequency: 5, category: 'primary' },
- { name: '', hex: '#0000ff', rgb: { r: 0, g: 0, b: 255 }, hsl: { h: 240, s: 100, l: 50 }, frequency: 2, category: 'primary' },
+ { name: '', hex: '#ff0000', frequency: 10, category: 'primary' },
+ { name: '', hex: '#00ff00', frequency: 5, category: 'primary' },
+ { name: '', hex: '#0000ff', frequency: 2, category: 'primary' },
];
const result = classifyColors(colors);
expect(result[0].category).toBe('primary');
@@ -212,9 +212,9 @@ describe('classifyColors', () => {
it('classifies neutrals by luminance', () => {
const colors: ColorToken[] = [
- { name: '', hex: '#ffffff', rgb: { r: 255, g: 255, b: 255 }, hsl: { h: 0, s: 0, l: 100 }, frequency: 10, category: 'primary' },
- { name: '', hex: '#000000', rgb: { r: 0, g: 0, b: 0 }, hsl: { h: 0, s: 0, l: 0 }, frequency: 5, category: 'primary' },
- { name: '', hex: '#808080', rgb: { r: 128, g: 128, b: 128 }, hsl: { h: 0, s: 0, l: 50 }, frequency: 3, category: 'primary' },
+ { name: '', hex: '#ffffff', frequency: 10, category: 'primary' },
+ { name: '', hex: '#000000', frequency: 5, category: 'primary' },
+ { name: '', hex: '#808080', frequency: 3, category: 'primary' },
];
const result = classifyColors(colors);
const white = result.find((c) => c.hex === '#ffffff')!;
diff --git a/src/lib/__tests__/design-tokens.test.ts b/src/lib/__tests__/design-tokens.test.ts
index 40fbae1..debdf36 100644
--- a/src/lib/__tests__/design-tokens.test.ts
+++ b/src/lib/__tests__/design-tokens.test.ts
@@ -12,8 +12,6 @@ const mockDS: DesignSystem = {
{
name: 'Brand Red',
hex: '#ff0000',
- rgb: { r: 255, g: 0, b: 0 },
- hsl: { h: 0, s: 100, l: 50 },
frequency: 10,
category: 'primary',
},
diff --git a/src/lib/__tests__/dom-utils.test.ts b/src/lib/__tests__/dom-utils.test.ts
index 7d2bd17..eca4209 100644
--- a/src/lib/__tests__/dom-utils.test.ts
+++ b/src/lib/__tests__/dom-utils.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
-import { isElementVisible, isPixelLensElement, getElementPath, getVisibleElements } from '../dom-utils';
+import { isElementVisible, isPixelLensElement, getVisibleElements } from '../dom-utils';
function mockRect(el: Element, width: number, height: number) {
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
@@ -88,51 +88,6 @@ describe('isPixelLensElement', () => {
});
});
-describe('getElementPath', () => {
- beforeEach(() => {
- document.body.innerHTML = '';
- });
-
- it('returns tag name for simple element', () => {
- const el = document.createElement('div');
- document.body.appendChild(el);
- const path = getElementPath(el);
- expect(path).toContain('div');
- });
-
- it('includes id when present and stops there', () => {
- const parent = document.createElement('div');
- parent.id = 'main';
- const child = document.createElement('span');
- parent.appendChild(child);
- document.body.appendChild(parent);
- const path = getElementPath(child);
- expect(path).toContain('div#main');
- expect(path).toContain('span');
- });
-
- it('includes class names', () => {
- const el = document.createElement('div');
- el.className = 'foo bar';
- document.body.appendChild(el);
- const path = getElementPath(el);
- expect(path).toContain('.foo');
- expect(path).toContain('.bar');
- });
-
- it('includes nth-of-type for siblings', () => {
- const parent = document.createElement('div');
- const child1 = document.createElement('span');
- const child2 = document.createElement('span');
- parent.appendChild(child1);
- parent.appendChild(child2);
- document.body.appendChild(parent);
- const path = getElementPath(child2);
- expect(path).toContain('nth-of-type(2)');
- });
-});
-
-
describe('getVisibleElements', () => {
beforeEach(() => {
vi.restoreAllMocks();
diff --git a/src/lib/css-parser.ts b/src/lib/css-parser.ts
index 9142bcb..f59416d 100644
--- a/src/lib/css-parser.ts
+++ b/src/lib/css-parser.ts
@@ -1,67 +1,5 @@
// PixelLens — CSS Parser Utilities
-import type { ColorInfo, TypographyInfo, EffectsInfo } from '@/types/inspection'
-import { toHex, toRgb, toHsl } from './colors'
-
-const COLOR_PROPERTIES = [
- 'color',
- 'background-color',
- 'border-color',
- 'border-top-color',
- 'border-right-color',
- 'border-bottom-color',
- 'border-left-color',
- 'outline-color',
- 'text-decoration-color',
-]
-
-export interface ParsedStyles {
- colors: ColorInfo[]
- typography: TypographyInfo
- effects: EffectsInfo
- allProperties: Record
-}
-
-export function parseComputedStyles(element: Element): ParsedStyles {
- const computed = window.getComputedStyle(element)
- const allProperties: Record = {}
-
- for (const prop of computed) {
- allProperties[prop] = computed.getPropertyValue(prop)
- }
-
- const colors: ColorInfo[] = COLOR_PROPERTIES
- .map((prop) => {
- const value = computed.getPropertyValue(prop)
- if (!value || value === 'transparent' || value === 'rgba(0, 0, 0, 0)') return null
- return {
- property: prop,
- value,
- hex: toHex(value),
- rgb: toRgb(value),
- hsl: toHsl(value),
- }
- })
- .filter((c): c is ColorInfo => c !== null)
-
- const typography: TypographyInfo = {
- fontFamily: computed.getPropertyValue('font-family'),
- fontSize: computed.getPropertyValue('font-size'),
- fontWeight: computed.getPropertyValue('font-weight'),
- lineHeight: computed.getPropertyValue('line-height'),
- letterSpacing: computed.getPropertyValue('letter-spacing'),
- }
-
- const effects: EffectsInfo = {
- boxShadow: computed.getPropertyValue('box-shadow'),
- opacity: computed.getPropertyValue('opacity'),
- backdropFilter: computed.getPropertyValue('backdrop-filter'),
- borderRadius: computed.getPropertyValue('border-radius'),
- }
-
- return { colors, typography, effects, allProperties }
-}
-
export function formatCSSProperty(prop: string, value: string): string {
return `${prop}: ${value};`
}
diff --git a/src/lib/dom-utils.ts b/src/lib/dom-utils.ts
index fbc0428..309d5d8 100644
--- a/src/lib/dom-utils.ts
+++ b/src/lib/dom-utils.ts
@@ -122,41 +122,3 @@ export function getFullComputedStyles(el: Element): InspectedElement {
},
}
}
-
-export function getElementPath(el: Element): string {
- const parts: string[] = []
- let current: Element | null = el
-
- while (current && current !== document.body) {
- let selector = current.tagName.toLowerCase()
-
- if (current.id) {
- selector += `#${current.id}`
- parts.unshift(selector)
- break
- }
-
- if (current.className && typeof current.className === 'string') {
- const classes = current.className.trim().split(/\s+/).slice(0, 2)
- if (classes.length > 0 && classes[0]) {
- selector += `.${classes.join('.')}`
- }
- }
-
- const parent = current.parentElement
- if (parent) {
- const siblings = Array.from(parent.children).filter(
- (c) => c.tagName === current!.tagName,
- )
- if (siblings.length > 1) {
- const index = siblings.indexOf(current) + 1
- selector += `:nth-of-type(${index})`
- }
- }
-
- parts.unshift(selector)
- current = current.parentElement
- }
-
- return parts.join(' > ')
-}
diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx
index cd6a5b4..5653407 100644
--- a/src/popup/Popup.tsx
+++ b/src/popup/Popup.tsx
@@ -14,10 +14,23 @@ import { MessageType } from '@/types/messages'
import { sendMessage } from '@/lib/messaging'
import { setPanelInitialMode } from '@/lib/storage'
import type { MarkdownResult } from '@/types/markdown'
+import PixelLensLogo from '@/sidepanel/components/PixelLensLogo'
type InspectStatus = 'idle' | 'active' | 'unsupported'
type MarkdownStatus = 'idle' | 'working' | 'done' | 'unsupported' | 'failed'
+function isMac(): boolean {
+ return typeof navigator !== 'undefined' && /Mac/i.test(navigator.userAgent)
+}
+
+// Fallback labels matching the manifest suggested_key (mac override included),
+// shown until chrome.commands.getAll() reports the real (possibly remapped) bindings.
+function defaultShortcuts() {
+ return isMac()
+ ? { inspect: '⌘⇧L', popup: '⌘⇧P' }
+ : { inspect: 'Ctrl+Shift+L', popup: 'Ctrl+Shift+P' }
+}
+
export function Popup() {
const [status, setStatus] = useState('idle')
const [currentUrl, setCurrentUrl] = useState('')
@@ -26,6 +39,7 @@ export function Popup() {
// chrome.sidePanel.open({ tabId }) synchronously — no awaited query is
// allowed before the open() or Chrome drops the user gesture.
const [tabId, setTabId] = useState(null)
+ const [shortcuts, setShortcuts] = useState(defaultShortcuts)
useEffect(() => {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
@@ -40,6 +54,19 @@ export function Popup() {
})
}, [])
+ // Show the shortcuts actually bound (the user may remap them in
+ // chrome://extensions/shortcuts); fall back to the manifest defaults.
+ useEffect(() => {
+ chrome.commands?.getAll?.((cmds) => {
+ const find = (name: string) => cmds.find((c) => c.name === name)?.shortcut || ''
+ const fallback = defaultShortcuts()
+ setShortcuts({
+ inspect: find('toggle-inspect') || fallback.inspect,
+ popup: find('_execute_action') || fallback.popup,
+ })
+ })
+ }, [])
+
async function handleInspect() {
const next: InspectStatus = status === 'active' ? 'idle' : 'active'
// Await the background's real delivery result. On unsupported pages
@@ -149,11 +176,7 @@ export function Popup() {
{/* Header */}
)}
+ {resultMode === 'fast' && emptySkipped > 0 && (
+
+ {emptySkipped.toLocaleString()} empty page{emptySkipped === 1 ? '' : 's'} — this site
+ renders with JavaScript. Switch “Render” to Full {' '}
+ below and crawl again.
+
+ )}
+
{stats.pageCount === 0 && stats.skippedCount === 0 && (
No pages could be converted — the site may be client-rendered (its initial HTML
@@ -308,6 +380,15 @@ function CrawlView() {
{/* Actions */}
+ {/* Render mode — Fast (fetch) vs Full (real browser tab). Takes effect on the next
+ crawl (Crawl again). Full is the fix for JS-rendered SPAs. */}
+
+
{/* Respect robots.txt — ON by default (safe). Turn OFF to include pages that
robots.txt blocks: this is a human converting pages they can already see,
not an indexing crawler. Takes effect on the next crawl (Crawl again). */}
diff --git a/src/sidepanel/views/DesignSystemView.tsx b/src/sidepanel/views/DesignSystemView.tsx
index d7def09..2f47559 100644
--- a/src/sidepanel/views/DesignSystemView.tsx
+++ b/src/sidepanel/views/DesignSystemView.tsx
@@ -4,12 +4,15 @@ import { usePanelStore } from '../store'
import ExportButton from '../components/ExportButton'
import ColorSwatch from '../components/ColorSwatch'
import ShadowPreview from '../components/ShadowPreview'
+import StaleScanBanner from '../components/StaleScanBanner'
+import { isDifferentHost } from '@/lib/url'
import type { DesignSystem } from '@/types/design-system'
function DesignSystemView() {
const designSystem = usePanelStore((s) => s.designSystem)
const setDesignSystem = usePanelStore((s) => s.setDesignSystem)
const setMode = usePanelStore((s) => s.setMode)
+ const activeTabUrl = usePanelStore((s) => s.activeTabUrl)
if (!designSystem) {
return (
@@ -49,8 +52,15 @@ function DesignSystemView() {
setDesignSystem(updated)
}
+ // Flag a design system restored from another site — the canonical scan CTA
+ // (with progress + error handling) lives in ScanView, so re-scan routes there.
+ const stale = isDifferentHost(designSystem.metadata.url, activeTabUrl)
+
return (
+ {stale && (
+
setMode('scan')} />
+ )}
{/* Header with export */}
diff --git a/src/sidepanel/views/MarkdownView.tsx b/src/sidepanel/views/MarkdownView.tsx
index 9405360..487188b 100644
--- a/src/sidepanel/views/MarkdownView.tsx
+++ b/src/sidepanel/views/MarkdownView.tsx
@@ -23,6 +23,8 @@ import { copyToClipboard, downloadMarkdown } from '@/lib/export'
import { buildLlmBundle } from '@/lib/llm-bundle'
import type { MarkdownFrontmatter, MarkdownResult } from '@/types/markdown'
import MarkdownPreview from '../components/MarkdownPreview'
+import StaleScanBanner from '../components/StaleScanBanner'
+import { isDifferentHost } from '@/lib/url'
// Build a safe .md filename from the page title (fallback: hostname).
function slugify(value: string): string {
@@ -62,6 +64,7 @@ function MarkdownView() {
const setLoading = usePanelStore((s) => s.setMarkdownLoading)
const setError = usePanelStore((s) => s.setMarkdownError)
const designSystem = usePanelStore((s) => s.designSystem)
+ const activeTabUrl = usePanelStore((s) => s.activeTabUrl)
const [copied, setCopied] = useState(false)
const [copiedLlm, setCopiedLlm] = useState(false)
@@ -191,8 +194,15 @@ function MarkdownView() {
setTimeout(() => setCopiedLlm(false), 1500)
}
+ // The persisted markdown result may be from a previously visited site; flag
+ // it rather than presenting it as the current page's conversion.
+ const stale = isDifferentHost(fm.url, activeTabUrl)
+
return (
+ {stale && (
+
+ )}
{/* Meta header — frontmatter + stats */}
diff --git a/src/sidepanel/views/ScanView.tsx b/src/sidepanel/views/ScanView.tsx
index 363364f..7b0f698 100644
--- a/src/sidepanel/views/ScanView.tsx
+++ b/src/sidepanel/views/ScanView.tsx
@@ -5,10 +5,12 @@ import { prefersReducedMotion } from '../reducedMotion'
import { usePanelStore } from '../store'
import { sendMessage } from '@/lib/messaging'
import { MessageType } from '@/types/messages'
+import { isDifferentHost } from '@/lib/url'
import ColorPalette from '../components/ColorPalette'
import TypeSpecimen from '../components/TypeSpecimen'
import SpacingScale from '../components/SpacingScale'
import ShadowPreview from '../components/ShadowPreview'
+import StaleScanBanner from '../components/StaleScanBanner'
type ScanTab = 'colors' | 'fonts' | 'spacing' | 'shadows'
@@ -25,6 +27,7 @@ function ScanView() {
const designSystem = usePanelStore((s) => s.designSystem)
const setMode = usePanelStore((s) => s.setMode)
const setScanError = usePanelStore((s) => s.setScanError)
+ const activeTabUrl = usePanelStore((s) => s.activeTabUrl)
const [activeTab, setActiveTab] = useState
('colors')
const tabsRef = useRef<(HTMLButtonElement | null)[]>([])
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 })
@@ -200,8 +203,16 @@ function ScanView() {
}
}
+ // The shown design system is the last scan globally; if that came from a
+ // different host than the tab on screen, flag it instead of passing it off
+ // as the current page.
+ const stale = isDifferentHost(designSystem.metadata.url, activeTabUrl)
+
return (
+ {stale && (
+
+ )}
{/* Scan result tabs */}
=
T extends MessageType.ELEMENT_SELECTED ? { received: boolean } :
T extends MessageType.SCAN_COMPLETE ? { received: boolean } :
T extends MessageType.EXTRACT_MARKDOWN ? MarkdownResult :
+ T extends MessageType.RENDER_PAGE ? RenderPageResponse :
T extends MessageType.PING ? { alive: boolean } :
{ success: boolean }