Skip to content

feat: implement complete PixelLens Chrome extension - #1

Merged
vgtray merged 2 commits into
mainfrom
feat/initial-extension
Apr 6, 2026
Merged

feat: implement complete PixelLens Chrome extension#1
vgtray merged 2 commits into
mainfrom
feat/initial-extension

Conversation

@vgtray

@vgtray vgtray commented Apr 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Complete Chrome Extension Manifest V3 for inspecting websites and extracting design systems
  • 54 files, 6655 lines of code, 68 unit tests, 0 TypeScript errors
  • Content scripts (inspector + scanner), Side Panel React app, Popup, Background SW

Changements

  • Content Scripts: Element inspector (hover highlight, click select, distance measurer, grid overlay), page scanner (color/typography/spacing extraction with Chroma.js clustering), floating toolbar + tooltip in Shadow DOM
  • Side Panel: React 19 + Zustand v5 app with 5 views (Inspector, Scan, DesignSystem, Export, History) and 8 reusable components
  • Animations: GSAP integrated for tab sliding indicator, progress bar shimmer, confetti export, hover scale, toolbar slide-up
  • Export: CSS Variables, Tailwind Config, JSON Tokens, PNG Palette
  • Tests: 68 Vitest unit tests covering all core libraries (colors, css-parser, design-tokens, dom-utils, storage)
  • CI: GitHub Actions workflow (typecheck → lint → test → build → artifact upload)

Tests

  • npx tsc --noEmit — 0 errors
  • npx vitest --run — 68/68 pass
  • Manual test: load extension in Chrome, inspect elements, scan page

🤖 Generated with Claude Code

Chrome Extension Manifest V3 for inspecting websites and extracting design systems.

- Content scripts: element inspector (hover highlight, click select, distance measurer, grid overlay), page scanner (colors, typography, spacing, shadows extraction with clustering), floating toolbar + tooltip in Shadow DOM
- Side panel: React 19 app with Inspector/Scan/DesignSystem/Export/History views, 8 reusable components (ColorSwatch, ColorPalette, TypeSpecimen, SpacingScale, BoxModelViz, CSSBlock, ShadowPreview, ExportButton)
- Background: service worker for message routing, commands, badge management
- Popup: compact dark UI with quick actions
- Shared libs: color utils (Chroma.js), CSS parser, design tokens generator (CSS vars/Tailwind/JSON), DOM utils, export, typed messaging, Chrome storage
- GSAP animations: tab sliding indicator, progress bar shimmer, confetti export, hover scale, toolbar slide-up
- 68 unit tests (Vitest) for all core libraries
- GitHub Actions CI: typecheck, lint, test, build, artifact upload
- Full README with setup instructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 6, 2026 23:09
@vgtray
vgtray merged commit 7fde463 into main Apr 6, 2026
1 of 2 checks passed
@vgtray
vgtray deleted the feat/initial-extension branch April 6, 2026 23:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a full Manifest V3 Chrome extension (“PixelLens”) for inspecting page styles and scanning pages to extract a design system, with a Side Panel UI, Popup UI, content-script tooling, export formats, unit tests, and CI automation.

Changes:

  • Adds Side Panel React app (Inspector/Scan/Design System/Export/History) and reusable UI components.
  • Implements content-script inspector tools (highlight/select/measure/grid) plus a full-page scanner (colors/typography/spacing/shadows/radius) that reports progress back to the panel.
  • Adds shared libraries (tokens export, CSS parsing, DOM utilities, storage/messaging) with Vitest coverage + GitHub Actions CI.

Reviewed changes

Copilot reviewed 52 out of 76 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
vitest.config.ts Configures Vitest (jsdom, globals) and @ path alias for tests.
src/sidepanel/views/ScanView.tsx Scan progress UI + tabbed results (colors/fonts/spacing/shadows).
src/sidepanel/views/InspectorView.tsx Element inspection view (colors/typography/box model/effects/raw CSS).
src/sidepanel/views/HistoryView.tsx Displays and manages scan history (store + storage merge).
src/sidepanel/views/ExportView.tsx Exports tokens in CSS/Tailwind/JSON via copy/download actions.
src/sidepanel/views/DesignSystemView.tsx Displays/edit tokens and provides export entry point.
src/sidepanel/styles/panel.css Side Panel theme tokens, base styles, and shared animations.
src/sidepanel/store.ts Zustand store for side panel modes, scan progress, and history.
src/sidepanel/main.tsx Side Panel React entrypoint and stylesheet wiring.
src/sidepanel/index.html Side Panel HTML host page.
src/sidepanel/components/TypeSpecimen.tsx Typography specimen preview + copy-to-clipboard.
src/sidepanel/components/SpacingScale.tsx Spacing scale visualization (frequency + base highlight).
src/sidepanel/components/ShadowPreview.tsx Shadow preview tiles with copy-to-clipboard.
src/sidepanel/components/ExportButton.tsx Dropdown export button with confetti feedback and PNG option.
src/sidepanel/components/CSSBlock.tsx Code preview block with copy action and basic highlighting.
src/sidepanel/components/ColorSwatch.tsx Color swatch with hover/copy interactions and format switching.
src/sidepanel/components/ColorPalette.tsx Groups and displays extracted colors by category.
src/sidepanel/components/BoxModelViz.tsx Renders margin/border/padding/content box-model visualization.
src/sidepanel/App.tsx Main panel shell, routing between views, and runtime message handling.
src/popup/styles/popup.css Popup styling aligned to the panel theme.
src/popup/Popup.tsx Popup UI for toggling inspect, scanning, and opening side panel.
src/popup/main.tsx Popup React entrypoint.
src/popup/index.html Popup HTML host page.
src/lib/storage.ts Chrome storage wrapper for preferences and saved design systems.
src/lib/messaging.ts Typed wrapper around Chrome runtime/tab messaging.
src/lib/export.ts Clipboard, file download, and palette PNG generation helpers.
src/lib/dom-utils.ts DOM traversal/visibility helpers and computed-style extraction.
src/lib/design-tokens.ts Converts extracted DS into CSS variables, Tailwind config, and JSON tokens.
src/lib/css-parser.ts Computed-style parsing/filtering and CSS block generation utilities.
src/lib/colors.ts Color conversion, clustering, categorization, and contrast helpers.
src/lib/tests/storage.test.ts Unit tests for storage wrapper behavior.
src/lib/tests/dom-utils.test.ts Unit tests for DOM utilities (visibility/path/PixelLens element checks).
src/lib/tests/design-tokens.test.ts Unit tests for token export formats and dispatcher.
src/lib/tests/css-parser.test.ts Unit tests for CSS formatting/filtering and shorthand expansion.
src/lib/tests/colors.test.ts Unit tests for color utilities (conversion, clustering, classification).
src/content/ui/InspectorTooltip.tsx Cursor-follow tooltip UI for inspect mode.
src/content/ui/FloatingToolbar.tsx Draggable in-page toolbar for switching modes and starting scans.
src/content/ui/ContentApp.tsx Shadow-DOM React wrapper that coordinates toolbar + tooltip.
src/content/scanner/TypographyExtractor.ts Extracts font families/variants and detects a type scale ratio.
src/content/scanner/SpacingExtractor.ts Extracts and normalizes spacing values and derives a spacing scale.
src/content/scanner/PageScanner.ts Orchestrates full scan phases and progress reporting.
src/content/scanner/DesignSystemBuilder.ts Assembles scan outputs and extracts shadows/border-radius tokens.
src/content/scanner/ColorExtractor.ts Extracts, clusters, and categorizes colors from computed styles.
src/content/inspector/GridOverlay.ts Canvas-based grid overlay with redraw scheduling.
src/content/inspector/ElementSelector.ts Captures element selection and sends inspected computed styles.
src/content/inspector/ElementHighlighter.ts Hover overlay for box model (content/padding/margin) + badge.
src/content/inspector/DistanceMeasurer.ts Measures distance between two clicked elements with guides.
src/content/index.ts Content script entry: mode management, message listeners, mounts Shadow UI.
src/background/service-worker.ts Background SW routing, inspect state per tab, side panel open, shortcuts.
README.md Project documentation (features, architecture, scripts, setup).
package.json Adds test/typecheck scripts and new dependencies/devDependencies.
package-lock.json Locks updated deps including Vitest/jsdom/coverage tooling.
.gitignore Adds ignore for PRD_02_PixelLens.mddist/.
.github/workflows/ci.yml CI pipeline: typecheck → lint → test → build → upload artifact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +37
case MessageType.TOGGLE_INSPECT: {
const tabId = sender.tab?.id
if (tabId) {
toggleInspect(tabId)
}
sendResponse({ success: true })

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MessageType.TOGGLE_INSPECT payload includes { active: boolean }, but this handler ignores payload.active and always calls toggleInspect(tabId). That can invert the requested state (e.g., callers sending active: false may end up enabling inspect), and can desync popup/toolbar UI from the actual tab state. Consider honoring payload.active by setting activeTabState explicitly (e.g., setInspect(tabId, payload.active)) and reserving toggling behavior for the keyboard command path.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +96
chrome.storage.sync.set({ pixellens_preferences: prefs.preferences })
sendResponse({ success: true })
break

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SET_PREFERENCES is typed as { preferences: Partial<Preferences> }, but this code overwrites pixellens_preferences with only the provided partial object. That can drop previously stored fields/defaults (e.g., setting colorFormat would erase gridSize). Merge with existing preferences before writing (or reuse the setPreferences helper in src/lib/storage.ts).

Suggested change
chrome.storage.sync.set({ pixellens_preferences: prefs.preferences })
sendResponse({ success: true })
break
chrome.storage.sync.get('pixellens_preferences', (result) => {
const mergedPreferences = {
colorFormat: 'hex',
gridSize: 8,
theme: 'dark',
...(result['pixellens_preferences'] || {}),
...prefs.preferences,
}
chrome.storage.sync.set({ pixellens_preferences: mergedPreferences }, () => {
sendResponse({ success: true })
})
})
return true // async response

Copilot uses AI. Check for mistakes.
Comment thread src/content/index.ts
Comment on lines +102 to +115
onMessage(MessageType.SCAN_PAGE, (_payload, _sender, sendResponse) => {
if (!scanner) {
scanner = new PageScanner()
}

scanner.scan((progress, phase) => {
sendMessage(MessageType.SCAN_PROGRESS, { progress, phase })
}).then((designSystem) => {
sendMessage(MessageType.SCAN_COMPLETE, { designSystem })
})

// Return true to keep the message channel open for async response
return true
})

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This SCAN_PAGE listener returns true (keeps the message channel open) but never calls sendResponse. In MV3 this can lead to pending/failed sendMessage calls ("message port closed" warnings) and unnecessary open ports. Either respond immediately (and don’t return true), or return true and call sendResponse in the then/catch of the scan promise.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +76
// Drag handling
const onMouseDown = useCallback((e: React.MouseEvent) => {
if (!toolbarRef.current) return
const rect = toolbarRef.current.getBoundingClientRect()
dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top }
setDragging(true)
}, [])

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drag start is attached to the entire toolbar (onMouseDown on the wrapper), so pressing any button will also start dragging because stopPropagation() is only applied to the button click handler, not to mousedown. This makes button clicks feel “sticky”/draggable. Consider starting drag only from a dedicated handle area, or early-return in onMouseDown when e.target is a button (or call e.stopPropagation() on the buttons’ onMouseDown).

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +72
<DSSection title="Palette" icon={<Palette size={14} />} count={designSystem.colors.length}>
<div className="flex flex-col gap-2">
{designSystem.colors.map((color, i) => (
<div key={i} className="flex items-center gap-2 group">
<ColorSwatch color={color.hex} size={28} />

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the array index as the React key here can cause incorrect row/component state when colors are renamed/removed (indices shift). Since these items are editable/removable, it’s safer to key by a stable identifier (e.g., a token id, or a combination derived from color.hex/category plus a disambiguator for duplicates).

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +117
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
}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isPixelLensElement is duplicated here, but the same helper already exists in src/lib/dom-utils.ts. Duplicating this logic risks divergence (e.g., if the host id changes or other PixelLens-owned roots are added). Prefer importing and reusing the shared helper.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +38
// Message routing between content script <-> side panel
chrome.runtime.onMessage.addListener((message: IncomingMessage, sender, sendResponse) => {
const { type, payload } = message

switch (type) {
case MessageType.TOGGLE_INSPECT: {
const tabId = sender.tab?.id
if (tabId) {
toggleInspect(tabId)
}
sendResponse({ success: true })
break

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sender.tab?.id will be undefined for messages coming from extension pages like the popup/sidepanel. As a result, TOGGLE_INSPECT triggered from src/popup/Popup.tsx (which uses chrome.runtime.sendMessage) won’t toggle anything. Consider falling back to querying the active tab when sender.tab is missing (or include an explicit tabId in the message payload).

Suggested change
// Message routing between content script <-> side panel
chrome.runtime.onMessage.addListener((message: IncomingMessage, sender, sendResponse) => {
const { type, payload } = message
switch (type) {
case MessageType.TOGGLE_INSPECT: {
const tabId = sender.tab?.id
if (tabId) {
toggleInspect(tabId)
}
sendResponse({ success: true })
break
async function resolveTabId(sender: chrome.runtime.MessageSender): Promise<number | undefined> {
if (sender.tab?.id) {
return sender.tab.id
}
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
return tab?.id
}
// Message routing between content script <-> side panel
chrome.runtime.onMessage.addListener((message: IncomingMessage, sender, sendResponse) => {
const { type, payload } = message
switch (type) {
case MessageType.TOGGLE_INSPECT: {
resolveTabId(sender)
.then((tabId) => {
if (tabId) {
toggleInspect(tabId)
}
sendResponse({ success: true })
})
.catch(() => {
sendResponse({ success: false })
})
return true

Copilot uses AI. Check for mistakes.
Comment thread README.md
## 📦 Installation

### Prérequis
- Node.js 20+

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package-lock.json includes dependencies (e.g., jsdom@29) that require Node ^20.19.0 (not just any 20.x). The README currently says “Node.js 20+”, which may lead to install failures on older Node 20 minors. Consider updating the prerequisite to the minimum supported Node version (or adding an explicit engines field in package.json).

Suggested change
- Node.js 20+
- Node.js 20.19.0+

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml

- uses: actions/setup-node@v4
with:
node-version: '20'

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several devDependencies in package-lock.json require Node ^20.19.0. Using node-version: '20' will usually pick the latest 20.x, but it’s safer/reproducible to pin to the minimum required minor (e.g., 20.19.0) or bump to Node 22+. Otherwise CI could break if it resolves to an older Node 20 minor in some environments.

Suggested change
node-version: '20'
node-version: '20.19.0'

Copilot uses AI. Check for mistakes.
Comment thread package.json
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:coverage": "vitest --coverage"
},

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given some locked dependencies require Node ^20.19.0 (see package-lock.json), it would help to declare a matching engines.node constraint here to prevent confusing installs on unsupported Node minors.

Suggested change
},
},
"engines": {
"node": "^20.19.0"
},

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants