From f673099e4b5d253fe0ba1d12cd3833c5b74311e2 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Sep 2025 17:58:07 -0700 Subject: [PATCH 001/109] Minor cleanup. --- browser-extension/src/entrypoints/background.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/browser-extension/src/entrypoints/background.ts b/browser-extension/src/entrypoints/background.ts index 3e071a1..6cb8e13 100644 --- a/browser-extension/src/entrypoints/background.ts +++ b/browser-extension/src/entrypoints/background.ts @@ -27,14 +27,13 @@ export function handleCommentEvent(message: CommentEvent, sender: any): boolean sender.tab?.windowId ) { if (message.type === 'ENHANCED') { - const tab: Tab = { - tabId: sender.tab.id, - windowId: sender.tab.windowId, - } const commentState: CommentState = { drafts: [], spot: message.spot, - tab, + tab: { + tabId: sender.tab.id, + windowId: sender.tab.windowId, + }, } openSpots.set(message.spot.unique_key, commentState) } else if (message.type === 'DESTROYED') { @@ -52,10 +51,7 @@ export function handlePopupMessage( sendResponse: (response: any) => void, ): typeof CLOSE_MESSAGE_PORT | typeof KEEP_PORT_OPEN { if (isGetOpenSpotsMessage(message)) { - const spots: CommentState[] = [] - for (const [, commentState] of openSpots) { - spots.push(commentState) - } + const spots: CommentState[] = Array.from(openSpots.values()) const response: GetOpenSpotsResponse = { spots } sendResponse(response) return KEEP_PORT_OPEN From b92a4a546b67ae178bf1f8159ed419241ee92918 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 11:46:32 -0700 Subject: [PATCH 002/109] Need to turn off biome `noUnknownAtRules` for tailwind v4 https://github.com/biomejs/biome/pull/7164 --- browser-extension/biome.json | 1 + 1 file changed, 1 insertion(+) diff --git a/browser-extension/biome.json b/browser-extension/biome.json index d4b34d1..54b9867 100644 --- a/browser-extension/biome.json +++ b/browser-extension/biome.json @@ -66,6 +66,7 @@ } }, "noExplicitAny": "off", + "noUnknownAtRules": "off", "noVar": "error" } } From 423a7bc39c7adf89062c4276645a8586f31f6819 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 11:47:26 -0700 Subject: [PATCH 003/109] Working straight from claude, refinement needed. --- browser-extension/package.json | 13 + browser-extension/postcss.config.cjs | 6 + browser-extension/src/components/ui/table.tsx | 91 ++ .../src/entrypoints/popup/index.html | 2 +- .../src/entrypoints/popup/main.ts | 97 --- .../src/entrypoints/popup/main.tsx | 143 +++ .../src/entrypoints/popup/style.css | 49 +- browser-extension/src/lib/enhancer.ts | 3 +- ...ddComment.ts => githubIssueAddComment.tsx} | 10 +- ...PRAddComment.ts => githubPRAddComment.tsx} | 10 +- browser-extension/src/lib/utils.ts | 6 + browser-extension/tailwind.config.cjs | 10 + browser-extension/tsconfig.json | 6 +- browser-extension/wxt.config.ts | 13 + pnpm-lock.yaml | 821 +++++++++++++++++- 15 files changed, 1112 insertions(+), 168 deletions(-) create mode 100644 browser-extension/postcss.config.cjs create mode 100644 browser-extension/src/components/ui/table.tsx delete mode 100644 browser-extension/src/entrypoints/popup/main.ts create mode 100644 browser-extension/src/entrypoints/popup/main.tsx rename browser-extension/src/lib/enhancers/github/{githubIssueAddComment.ts => githubIssueAddComment.tsx} (88%) rename browser-extension/src/lib/enhancers/github/{githubPRAddComment.ts => githubPRAddComment.tsx} (89%) create mode 100644 browser-extension/src/lib/utils.ts create mode 100644 browser-extension/tailwind.config.cjs diff --git a/browser-extension/package.json b/browser-extension/package.json index 128aa79..92790c0 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,23 +1,36 @@ { "author": "DiffPlug", "dependencies": { + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", "@wxt-dev/webextension-polyfill": "^1.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "highlight.js": "^11.11.1", + "lucide-react": "^0.543.0", "overtype": "workspace:*", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwind-merge": "^3.3.1", "webextension-polyfill": "^0.12.0" }, "description": "Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).", "devDependencies": { "@biomejs/biome": "^2.1.2", "@playwright/test": "^1.46.0", + "@tailwindcss/postcss": "^4.1.13", "@testing-library/jest-dom": "^6.6.4", "@types/express": "^4.17.21", "@types/har-format": "^1.2.16", "@types/node": "^22.16.5", + "@vitejs/plugin-react": "^5.0.2", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", + "autoprefixer": "^10.4.21", "express": "^4.19.2", "linkedom": "^0.18.12", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.13", "tsx": "^4.19.1", "typescript": "^5.8.3", "vitest": "^3.2.4", diff --git a/browser-extension/postcss.config.cjs b/browser-extension/postcss.config.cjs new file mode 100644 index 0000000..dc655aa --- /dev/null +++ b/browser-extension/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/browser-extension/src/components/ui/table.tsx b/browser-extension/src/components/ui/table.tsx new file mode 100644 index 0000000..e8548bf --- /dev/null +++ b/browser-extension/src/components/ui/table.tsx @@ -0,0 +1,91 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +) +Table.displayName = 'Table' + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = 'TableHeader' + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = 'TableBody' + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', className)} + {...props} + /> +)) +TableFooter.displayName = 'TableFooter' + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +) +TableRow.displayName = 'TableRow' + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( + @@ -431,16 +455,16 @@ export const ClaudePrototype = () => { {draft.linkCount} )} - {draft.hasImage && ( + {draft.imageCount > 0 && ( - image + {draft.imageCount} )} - {draft.hasCode && ( + {draft.codeCount > 0 && ( - code + {draft.codeCount} )} {draft.charCount} chars From 0f3c0ccfd3e62310b2b047cb715a66ef4908c106 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 11:15:13 -0700 Subject: [PATCH 045/109] Fix the reddit example --- browser-extension/tests/playground/claude.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 8fb30a0..81549b3 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -437,18 +437,23 @@ export const ClaudePrototype = () => { href={draft.url} className='hover:underline truncate max-w-[28ch]' > - #{draft.number} {draft.repoSlug} + {draft.repoSlug.startsWith('r/') ? draft.repoSlug : + `#${draft.number} ${draft.repoSlug}` + } {/* Title + snippet */}
{draft.title} - — {draft.content.substring(0, 60)}… +
+
+ {draft.content.substring(0, 60)}…
{/* Signals row (hidden on small screens) */}
+ {draft.linkCount > 0 && ( From 703e615e83c1d47b4325e628148647a146737a25 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 11:26:28 -0700 Subject: [PATCH 046/109] Getting pretty darn good. --- browser-extension/tests/playground/claude.tsx | 90 ++++++++++--------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 81549b3..e9d1d18 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -7,6 +7,7 @@ import { Filter, Link, Search, + TextSelect, Trash2, } from 'lucide-react' import { IssueOpenedIcon, GitPullRequestIcon } from '@primer/octicons-react' @@ -423,57 +424,60 @@ export const ClaudePrototype = () => {
- @@ -402,12 +401,6 @@ export const ClaudePrototype = () => { )} - @@ -481,30 +474,30 @@ export const ClaudePrototype = () => { - From f74ec808da65b340306481f594a7c13b7ec61cd5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 13:38:52 -0700 Subject: [PATCH 048/109] Fixup --- browser-extension/tests/playground/claude.tsx | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 2b1fac5..58ebccc 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,27 +1,27 @@ +import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' import { ArrowDown, ArrowUp, - Image, Code, ExternalLink, Filter, + Image, Link, Search, TextSelect, Trash2, } from 'lucide-react' -import { IssueOpenedIcon, GitPullRequestIcon } from '@primer/octicons-react' import { useMemo, useState } from 'react' // Mock data generator const generateMockDrafts = () => [ { charCount: 245, + codeCount: 3, content: 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', - codeCount: 3, - imageCount: 2, id: '1', + imageCount: 2, kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 30, linkCount: 2, @@ -35,11 +35,11 @@ const generateMockDrafts = () => [ }, { charCount: 180, + codeCount: 0, content: "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", - codeCount: 0, - imageCount: 0, id: '2', + imageCount: 0, kind: 'Comment', lastEdit: Date.now() - 1000 * 60 * 60 * 2, linkCount: 1, @@ -51,11 +51,11 @@ const generateMockDrafts = () => [ }, { charCount: 456, + codeCount: 1, content: "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", - codeCount: 1, - imageCount: 0, id: '3', + imageCount: 0, kind: 'Issue', lastEdit: Date.now() - 1000 * 60 * 60 * 5, linkCount: 0, @@ -69,11 +69,11 @@ const generateMockDrafts = () => [ }, { charCount: 322, + codeCount: 0, content: 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', - codeCount: 0, - imageCount: 4, id: '4', + imageCount: 4, kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 60 * 24, linkCount: 3, @@ -87,11 +87,11 @@ const generateMockDrafts = () => [ }, { charCount: 678, + codeCount: 7, content: 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', - codeCount: 7, - imageCount: 1, id: '5', + imageCount: 1, kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 60 * 48, linkCount: 5, @@ -226,10 +226,7 @@ export const ClaudePrototype = () => { if ( filteredDrafts.length === 0 && - (searchQuery || - hasCodeFilter || - hasImageFilter || - hasLinkFilter) + (searchQuery || hasCodeFilter || hasImageFilter || hasLinkFilter) ) { return (
@@ -273,8 +270,7 @@ export const ClaudePrototype = () => {
{/* Header controls */}
-
-
+
{/* Bulk actions bar */} {selectedIds.size > 0 && ( @@ -390,7 +386,9 @@ export const ClaudePrototype = () => { >
@@ -475,7 +478,10 @@ export const ClaudePrototype = () => {
- {filteredDrafts.map((draft) => ( - - - - - - ))} + {filteredDrafts.map((draft) => + commentRow(draft, selectedIds, toggleSelection, handleOpen, handleTrash), + )}
+)) +TableHead.displayName = 'TableHead' + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = 'TableCell' + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = 'TableCaption' + +export { Table, TableHeader, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableRow } diff --git a/browser-extension/src/entrypoints/popup/index.html b/browser-extension/src/entrypoints/popup/index.html index af3b409..8a09020 100644 --- a/browser-extension/src/entrypoints/popup/index.html +++ b/browser-extension/src/entrypoints/popup/index.html @@ -8,6 +8,6 @@
- + \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/main.ts b/browser-extension/src/entrypoints/popup/main.ts deleted file mode 100644 index 4ccea9a..0000000 --- a/browser-extension/src/entrypoints/popup/main.ts +++ /dev/null @@ -1,97 +0,0 @@ -import './style.css' -import { logger } from '../../lib/logger' -import type { - GetOpenSpotsMessage, - GetOpenSpotsResponse, - SwitchToTabMessage, -} from '../../lib/messages' -import { EnhancerRegistry } from '../../lib/registries' -import type { CommentState } from '../background' - -// Test basic DOM access -try { - const app = document.getElementById('app')! - logger.debug('Found app element:', app) - app.innerHTML = '
Script is running...
' -} catch (error) { - logger.error('Error accessing DOM:', error) -} - -const enhancers = new EnhancerRegistry() - -async function getOpenSpots(): Promise { - logger.debug('Sending message to background script...') - try { - const message: GetOpenSpotsMessage = { type: 'GET_OPEN_SPOTS' } - const response = (await browser.runtime.sendMessage(message)) as GetOpenSpotsResponse - logger.debug('Received response:', response) - return response.spots || [] - } catch (error) { - logger.error('Error sending message to background:', error) - return [] - } -} - -function switchToTab(tabId: number, windowId: number): void { - // Send message to background script to handle tab switching - // This avoids the popup context being destroyed before completion - const message: SwitchToTabMessage = { - tabId, - type: 'SWITCH_TO_TAB', - windowId, - } - browser.runtime.sendMessage(message) - window.close() -} - -function createSpotElement(commentState: CommentState): HTMLElement { - const item = document.createElement('div') - item.className = 'spot-item' - - logger.debug('Creating spot element for:', commentState.spot) - const enhancer = enhancers.enhancerFor(commentState.spot) - if (!enhancer) { - logger.error('No enhancer found for:', commentState.spot) - logger.error('Only have enhancers for:', enhancers.byType) - } - - const title = document.createElement('div') - title.className = 'spot-title' - title.textContent = enhancer.tableTitle(commentState.spot) - item.appendChild(title) - item.addEventListener('click', () => { - switchToTab(commentState.tab.tabId, commentState.tab.windowId) - }) - return item -} - -async function renderOpenSpots(): Promise { - logger.debug('renderOpenSpots called') - const app = document.getElementById('app')! - const spots = await getOpenSpots() - logger.debug('Got spots:', spots) - - if (spots.length === 0) { - app.innerHTML = '
No open comment spots
' - return - } - - const header = document.createElement('h2') - header.textContent = 'Open Comment Spots' - app.appendChild(header) - - const list = document.createElement('div') - list.className = 'spots-list' - - spots.forEach((spot) => { - list.appendChild(createSpotElement(spot)) - }) - - app.appendChild(list) -} - -renderOpenSpots().catch((error) => { - logger.error('Error in renderOpenSpots:', error) - const app = document.getElementById('app')! - app.innerHTML = `
Error loading spots: ${error.message}
` -}) diff --git a/browser-extension/src/entrypoints/popup/main.tsx b/browser-extension/src/entrypoints/popup/main.tsx new file mode 100644 index 0000000..fb2290b --- /dev/null +++ b/browser-extension/src/entrypoints/popup/main.tsx @@ -0,0 +1,143 @@ +import './style.css' +import React from 'react' +import { createRoot } from 'react-dom/client' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { cn } from '@/lib/utils' +import { logger } from '../../lib/logger' +import type { + GetOpenSpotsMessage, + GetOpenSpotsResponse, + SwitchToTabMessage, +} from '../../lib/messages' +import { EnhancerRegistry } from '../../lib/registries' +import type { CommentState } from '../background' + +const enhancers = new EnhancerRegistry() + +async function getOpenSpots(): Promise { + logger.debug('Sending message to background script...') + try { + const message: GetOpenSpotsMessage = { type: 'GET_OPEN_SPOTS' } + const response = (await browser.runtime.sendMessage(message)) as GetOpenSpotsResponse + logger.debug('Received response:', response) + return response.spots || [] + } catch (error) { + logger.error('Error sending message to background:', error) + return [] + } +} + +function switchToTab(tabId: number, windowId: number): void { + const message: SwitchToTabMessage = { + tabId, + type: 'SWITCH_TO_TAB', + windowId, + } + browser.runtime.sendMessage(message) + window.close() +} + +interface SpotRowProps { + commentState: CommentState + onClick: () => void +} + +function SpotRow({ commentState, onClick }: SpotRowProps) { + const enhancer = enhancers.enhancerFor(commentState.spot) + + if (!enhancer) { + logger.error('No enhancer found for:', commentState.spot) + logger.error('Only have enhancers for:', enhancers.byType) + return null + } + + return ( + + +
+ {enhancer.tableIcon(commentState.spot)} +
+ {enhancer.tableTitle(commentState.spot)} +
+
+
+
+ ) +} + +function PopupApp() { + const [spots, setSpots] = React.useState([]) + const [isLoading, setIsLoading] = React.useState(true) + + React.useEffect(() => { + const loadSpots = async () => { + try { + const openSpots = await getOpenSpots() + setSpots(openSpots) + } catch (error) { + logger.error('Error loading spots:', error) + } finally { + setIsLoading(false) + } + } + + loadSpots() + }, []) + + if (isLoading) { + return
Loading...
+ } + + if (spots.length === 0) { + return ( +
No open comment spots
+ ) + } + + return ( +
+

Open Comment Spots

+ +
+ + + + Comment Spots + + + + {spots.map((spot) => ( + switchToTab(spot.tab.tabId, spot.tab.windowId)} + /> + ))} + +
+
+
+ ) +} + +// Initialize React app +const app = document.getElementById('app') +if (app) { + const root = createRoot(app) + root.render() +} else { + logger.error('App element not found') +} diff --git a/browser-extension/src/entrypoints/popup/style.css b/browser-extension/src/entrypoints/popup/style.css index 0d63390..b773d69 100644 --- a/browser-extension/src/entrypoints/popup/style.css +++ b/browser-extension/src/entrypoints/popup/style.css @@ -1,3 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + body { width: 300px; padding: 15px; @@ -6,48 +10,3 @@ body { line-height: 1.4; margin: 0; } - -h2 { - margin: 0 0 15px 0; - font-size: 16px; - font-weight: 600; - color: #333; -} - -.spots-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.spot-item { - padding: 10px; - border: 1px solid #e0e0e0; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s ease; - background: white; -} - -.spot-item:hover { - background: #f5f5f5; - border-color: #d0d0d0; - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.spot-title { - font-weight: 500; - color: #333; - margin-bottom: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.no-spots { - text-align: center; - color: #666; - padding: 40px 20px; - font-style: italic; -} diff --git a/browser-extension/src/lib/enhancer.ts b/browser-extension/src/lib/enhancer.ts index 0895454..8d5f661 100644 --- a/browser-extension/src/lib/enhancer.ts +++ b/browser-extension/src/lib/enhancer.ts @@ -1,4 +1,5 @@ import type { OverTypeInstance } from 'overtype' +import type { ReactNode } from 'react' /** * Stores enough info about the location of a draft to: @@ -42,5 +43,5 @@ export interface CommentEnhancer { enhance(textarea: HTMLTextAreaElement, spot: Spot): OverTypeInstance tableIcon(spot: Spot): string - tableTitle(spot: Spot): string + tableTitle(spot: Spot): ReactNode } diff --git a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.ts b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx similarity index 88% rename from browser-extension/src/lib/enhancers/github/githubIssueAddComment.ts rename to browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx index bfdc02f..b17b7f7 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.ts +++ b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx @@ -1,4 +1,5 @@ import OverType, { type OverTypeInstance } from 'overtype' +import type React from 'react' import type { CommentEnhancer, CommentSpot } from '../../enhancer' import { logger } from '../../logger' import { modifyDOM } from '../modifyDOM' @@ -54,9 +55,14 @@ export class GitHubIssueAddCommentEnhancer implements CommentEnhancer + {slug} + Issue #{number} + + ) } tableIcon(_: GitHubIssueAddCommentSpot): string { diff --git a/browser-extension/src/lib/enhancers/github/githubPRAddComment.ts b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx similarity index 89% rename from browser-extension/src/lib/enhancers/github/githubPRAddComment.ts rename to browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx index 8514000..a4ba1bc 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRAddComment.ts +++ b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx @@ -1,4 +1,5 @@ import OverType, { type OverTypeInstance } from 'overtype' +import type React from 'react' import type { CommentEnhancer, CommentSpot } from '../../enhancer' import { logger } from '../../logger' import { modifyDOM } from '../modifyDOM' @@ -58,9 +59,14 @@ export class GitHubPRAddCommentEnhancer implements CommentEnhancer + {slug} + PR #{number} + + ) } tableIcon(_: GitHubPRAddCommentSpot): string { diff --git a/browser-extension/src/lib/utils.ts b/browser-extension/src/lib/utils.ts new file mode 100644 index 0000000..d32b0fe --- /dev/null +++ b/browser-extension/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/browser-extension/tailwind.config.cjs b/browser-extension/tailwind.config.cjs new file mode 100644 index 0000000..08621b4 --- /dev/null +++ b/browser-extension/tailwind.config.cjs @@ -0,0 +1,10 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/browser-extension/tsconfig.json b/browser-extension/tsconfig.json index 90169d5..d0ce398 100644 --- a/browser-extension/tsconfig.json +++ b/browser-extension/tsconfig.json @@ -8,9 +8,13 @@ "esModuleInterop": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, - "jsx": "preserve", + "jsx": "react-jsx", "lib": ["ES2022", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, // Emit "noEmit": true, diff --git a/browser-extension/wxt.config.ts b/browser-extension/wxt.config.ts index 7a4699a..826fb93 100644 --- a/browser-extension/wxt.config.ts +++ b/browser-extension/wxt.config.ts @@ -1,6 +1,19 @@ import { defineConfig } from 'wxt' +import react from '@vitejs/plugin-react' +import path from 'path' export default defineConfig({ + vite: () => ({ + plugins: [react()], + css: { + postcss: path.resolve('./postcss.config.cjs') + }, + resolve: { + alias: { + '@': path.resolve('./src') + } + } + }), manifest: { description: 'Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8aa1ce..bf482a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,15 +10,39 @@ importers: browser-extension: dependencies: + '@types/react': + specifier: ^19.1.12 + version: 19.1.12 + '@types/react-dom': + specifier: ^19.1.9 + version: 19.1.9(@types/react@19.1.12) '@wxt-dev/webextension-polyfill': specifier: ^1.0.0 - version: 1.0.0(webextension-polyfill@0.12.0)(wxt@0.20.11(@types/node@22.18.1)(jiti@2.5.1)(rollup@4.50.1)(tsx@4.20.5)) + version: 1.0.0(webextension-polyfill@0.12.0)(wxt@0.20.11(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(tsx@4.20.5)) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 highlight.js: specifier: ^11.11.1 version: 11.11.1 + lucide-react: + specifier: ^0.543.0 + version: 0.543.0(react@19.1.1) overtype: specifier: workspace:* version: link:../packages/overtype + react: + specifier: ^19.1.1 + version: 19.1.1 + react-dom: + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 webextension-polyfill: specifier: ^0.12.0 version: 0.12.0 @@ -29,6 +53,9 @@ importers: '@playwright/test': specifier: ^1.46.0 version: 1.55.0 + '@tailwindcss/postcss': + specifier: ^4.1.13 + version: 4.1.13 '@testing-library/jest-dom': specifier: ^6.6.4 version: 6.8.0 @@ -41,18 +68,30 @@ importers: '@types/node': specifier: ^22.16.5 version: 22.18.1 + '@vitejs/plugin-react': + specifier: ^5.0.2 + version: 5.0.2(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5)) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) '@vitest/ui': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) express: specifier: ^4.19.2 version: 4.21.2 linkedom: specifier: ^0.18.12 version: 0.18.12 + postcss: + specifier: ^8.5.6 + version: 8.5.6 + tailwindcss: + specifier: ^4.1.13 + version: 4.1.13 tsx: specifier: ^4.19.1 version: 4.20.5 @@ -61,10 +100,10 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(tsx@4.20.5) + version: 3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.5) wxt: specifier: ^0.20.7 - version: 0.20.11(@types/node@22.18.1)(jiti@2.5.1)(rollup@4.50.1)(tsx@4.20.5) + version: 0.20.11(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(tsx@4.20.5) packages/overtype: dependencies: @@ -106,6 +145,10 @@ packages: rollup: optional: true + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -117,6 +160,40 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -125,15 +202,43 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.28.4': resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.2': resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.4': resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} @@ -542,6 +647,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -598,6 +707,9 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rolldown/pluginutils@1.0.0-beta.34': + resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} + '@rollup/rollup-android-arm-eabi@4.50.1': resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} cpu: [arm] @@ -703,10 +815,110 @@ packages: cpu: [x64] os: [win32] + '@tailwindcss/node@4.1.13': + resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==} + + '@tailwindcss/oxide-android-arm64@4.1.13': + resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.13': + resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.13': + resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.13': + resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.13': + resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.13': + resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.13': + resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.13': + resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==} + '@testing-library/jest-dom@6.8.0': resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -761,6 +973,14 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.1.9': + resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.12': + resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} + '@types/send@0.17.5': resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} @@ -770,6 +990,12 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@vitejs/plugin-react@5.0.2': + resolution: {integrity: sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/coverage-v8@3.2.4': resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} peerDependencies: @@ -916,6 +1142,13 @@ packages: atomically@2.0.3: resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -953,6 +1186,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.25.4: + resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -1002,6 +1240,9 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} + caniuse-lite@1.0.30001741: + resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1022,6 +1263,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + chrome-launcher@1.2.0: resolution: {integrity: sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==} engines: {node: '>=12.13.0'} @@ -1034,6 +1279,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -1070,6 +1318,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1120,6 +1372,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -1155,6 +1410,9 @@ packages: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -1248,6 +1506,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} @@ -1293,6 +1555,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.215: + resolution: {integrity: sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==} + emoji-regex@10.5.0: resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} @@ -1313,6 +1578,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1475,6 +1744,9 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -1500,6 +1772,10 @@ packages: resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==} hasBin: true + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1846,6 +2122,11 @@ packages: canvas: optional: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -1886,6 +2167,70 @@ packages: lighthouse-logger@2.0.2: resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1944,10 +2289,18 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lucide-react@0.543.0: + resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} @@ -2052,6 +2405,15 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -2095,6 +2457,9 @@ packages: node-notifier@10.0.1: resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + node-releases@2.0.20: + resolution: {integrity: sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==} + normalize-package-data@3.0.3: resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} engines: {node: '>=10'} @@ -2103,6 +2468,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -2285,6 +2654,9 @@ packages: resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==} engines: {node: '>= 10.12'} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2366,6 +2738,19 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@19.1.1: + resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + peerDependencies: + react: ^19.1.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + engines: {node: '>=0.10.0'} + read-pkg-up@8.0.0: resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} engines: {node: '>=12'} @@ -2465,12 +2850,19 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} secure-compare@3.0.1: resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -2666,6 +3058,20 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwindcss@4.1.13: + resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} + + tapable@2.2.3: + resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -2808,6 +3214,12 @@ packages: resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} engines: {node: '>=18.12.0'} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + update-notifier@7.3.1: resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} engines: {node: '>=18'} @@ -3032,9 +3444,16 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -3082,6 +3501,8 @@ snapshots: optionalDependencies: rollup: 4.50.1 + '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -3101,16 +3522,109 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + '@babel/parser@7.28.4': dependencies: '@babel/types': 7.28.4 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/runtime@7.28.2': {} + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -3351,6 +3865,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@istanbuljs/schema@0.1.3': {} '@jridgewell/gen-mapping@0.3.13': @@ -3405,6 +3923,8 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@rolldown/pluginutils@1.0.0-beta.34': {} + '@rollup/rollup-android-arm-eabi@4.50.1': optional: true @@ -3468,6 +3988,78 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true + '@tailwindcss/node@4.1.13': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.19 + source-map-js: 1.2.1 + tailwindcss: 4.1.13 + + '@tailwindcss/oxide-android-arm64@4.1.13': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.13': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.13': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.13': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.13': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + optional: true + + '@tailwindcss/oxide@4.1.13': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.13 + '@tailwindcss/oxide-darwin-arm64': 4.1.13 + '@tailwindcss/oxide-darwin-x64': 4.1.13 + '@tailwindcss/oxide-freebsd-x64': 4.1.13 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.13 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.13 + '@tailwindcss/oxide-linux-x64-musl': 4.1.13 + '@tailwindcss/oxide-wasm32-wasi': 4.1.13 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 + + '@tailwindcss/postcss@4.1.13': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.13 + '@tailwindcss/oxide': 4.1.13 + postcss: 8.5.6 + tailwindcss: 4.1.13 + '@testing-library/jest-dom@6.8.0': dependencies: '@adobe/css-tools': 4.4.4 @@ -3477,6 +4069,27 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -3534,6 +4147,14 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-dom@19.1.9(@types/react@19.1.12)': + dependencies: + '@types/react': 19.1.12 + + '@types/react@19.1.12': + dependencies: + csstype: 3.1.3 + '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 @@ -3550,6 +4171,18 @@ snapshots: '@types/node': 22.18.1 optional: true + '@vitejs/plugin-react@5.0.2(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.34 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 @@ -3565,7 +4198,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(tsx@4.20.5) + vitest: 3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.5) transitivePeerDependencies: - supports-color @@ -3577,13 +4210,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5))': + '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5) + vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) '@vitest/pretty-format@3.2.4': dependencies: @@ -3614,7 +4247,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(tsx@4.20.5) + vitest: 3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.5) '@vitest/utils@3.2.4': dependencies: @@ -3643,10 +4276,10 @@ snapshots: async-mutex: 0.5.0 dequal: 2.0.3 - '@wxt-dev/webextension-polyfill@1.0.0(webextension-polyfill@0.12.0)(wxt@0.20.11(@types/node@22.18.1)(jiti@2.5.1)(rollup@4.50.1)(tsx@4.20.5))': + '@wxt-dev/webextension-polyfill@1.0.0(webextension-polyfill@0.12.0)(wxt@0.20.11(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(tsx@4.20.5))': dependencies: webextension-polyfill: 0.12.0 - wxt: 0.20.11(@types/node@22.18.1)(jiti@2.5.1)(rollup@4.50.1)(tsx@4.20.5) + wxt: 0.20.11(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(tsx@4.20.5) accepts@1.3.8: dependencies: @@ -3710,6 +4343,16 @@ snapshots: stubborn-fs: 1.2.5 when-exit: 2.1.4 + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.25.4 + caniuse-lite: 1.0.30001741 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -3769,6 +4412,13 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.25.4: + dependencies: + caniuse-lite: 1.0.30001741 + electron-to-chromium: 1.5.215 + node-releases: 2.0.20 + update-browserslist-db: 1.1.3(browserslist@4.25.4) + buffer-crc32@0.2.13: {} buffer-from@1.1.2: {} @@ -3824,6 +4474,8 @@ snapshots: camelcase@8.0.0: {} + caniuse-lite@1.0.30001741: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3845,6 +4497,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@3.0.0: {} + chrome-launcher@1.2.0: dependencies: '@types/node': 22.18.1 @@ -3860,6 +4514,10 @@ snapshots: dependencies: consola: 3.4.2 + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + cli-boxes@3.0.0: {} cli-cursor@4.0.0: @@ -3900,6 +4558,8 @@ snapshots: clone@1.0.4: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3947,6 +4607,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.0.6: {} cookie@0.7.1: {} @@ -3980,6 +4642,8 @@ snapshots: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 + csstype@3.1.3: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -4039,6 +4703,8 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.0.4: {} + dom-accessibility-api@0.6.3: {} dom-serializer@2.0.0: @@ -4083,6 +4749,8 @@ snapshots: ee-first@1.1.1: {} + electron-to-chromium@1.5.215: {} + emoji-regex@10.5.0: {} emoji-regex@8.0.0: {} @@ -4097,6 +4765,11 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.3 + entities@4.5.0: {} entities@6.0.1: {} @@ -4312,6 +4985,8 @@ snapshots: forwarded@0.2.0: {} + fraction.js@4.3.7: {} + fresh@0.5.2: {} fs-extra@11.3.1: @@ -4337,6 +5012,8 @@ snapshots: which: 1.2.4 winreg: 0.0.12 + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.3.1: {} @@ -4678,6 +5355,8 @@ snapshots: - supports-color - utf-8-validate + jsesc@3.1.0: {} + json-parse-even-better-errors@2.3.1: {} json-parse-even-better-errors@3.0.2: {} @@ -4718,6 +5397,51 @@ snapshots: transitivePeerDependencies: - supports-color + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + lines-and-columns@1.2.4: {} lines-and-columns@2.0.4: {} @@ -4779,10 +5503,18 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lru-cache@6.0.0: dependencies: yallist: 4.0.0 + lucide-react@0.543.0(react@19.1.1): + dependencies: + react: 19.1.1 + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4875,6 +5607,12 @@ snapshots: minipass@7.1.2: {} + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -4920,6 +5658,8 @@ snapshots: uuid: 8.3.2 which: 2.0.2 + node-releases@2.0.20: {} + normalize-package-data@3.0.3: dependencies: hosted-git-info: 4.1.0 @@ -4929,6 +5669,8 @@ snapshots: normalize-path@3.0.0: {} + normalize-range@0.1.2: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -5136,6 +5878,8 @@ snapshots: transitivePeerDependencies: - supports-color + postcss-value-parser@4.2.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -5232,6 +5976,15 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-dom@19.1.1(react@19.1.1): + dependencies: + react: 19.1.1 + scheduler: 0.26.0 + + react-refresh@0.17.0: {} + + react@19.1.1: {} + read-pkg-up@8.0.0: dependencies: find-up: 5.0.0 @@ -5352,10 +6105,14 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.26.0: {} + scule@1.3.0: {} secure-compare@3.0.1: {} + semver@6.3.1: {} + semver@7.7.2: {} send@0.19.0: @@ -5568,6 +6325,21 @@ snapshots: symbol-tree@3.2.4: {} + tailwind-merge@3.3.1: {} + + tailwindcss@4.1.13: {} + + tapable@2.2.3: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -5698,6 +6470,12 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.1.3(browserslist@4.25.4): + dependencies: + browserslist: 4.25.4 + escalade: 3.2.0 + picocolors: 1.1.1 + update-notifier@7.3.1: dependencies: boxen: 8.0.1 @@ -5726,13 +6504,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.2.4(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5): + vite-node@3.2.4(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5) + vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) transitivePeerDependencies: - '@types/node' - jiti @@ -5747,7 +6525,7 @@ snapshots: - tsx - yaml - vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5): + vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -5759,13 +6537,14 @@ snapshots: '@types/node': 22.18.1 fsevents: 2.3.3 jiti: 2.5.1 + lightningcss: 1.30.1 tsx: 4.20.5 - vitest@3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(tsx@4.20.5): + vitest@3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.5): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5)) + '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -5783,8 +6562,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5) - vite-node: 3.2.4(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5) + vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) + vite-node: 3.2.4(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.18.1 @@ -5913,7 +6692,7 @@ snapshots: dependencies: is-wsl: 3.1.0 - wxt@0.20.11(@types/node@22.18.1)(jiti@2.5.1)(rollup@4.50.1)(tsx@4.20.5): + wxt@0.20.11(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(tsx@4.20.5): dependencies: '@1natsu/wait-element': 4.1.2 '@aklinker1/rollup-plugin-visualizer': 5.12.0(rollup@4.50.1) @@ -5957,8 +6736,8 @@ snapshots: publish-browser-extension: 3.0.2 scule: 1.3.0 unimport: 5.2.0 - vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5) - vite-node: 3.2.4(@types/node@22.18.1)(jiti@2.5.1)(tsx@4.20.5) + vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) + vite-node: 3.2.4(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) web-ext-run: 0.2.4 transitivePeerDependencies: - '@types/node' @@ -5991,8 +6770,12 @@ snapshots: y18n@5.0.8: {} + yallist@3.1.1: {} + yallist@4.0.0: {} + yallist@5.0.0: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} From 9377e7a761f614abd002592dc799d2c5dca92f51 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 11:53:50 -0700 Subject: [PATCH 004/109] `CommentEnhancer` now just has `tableRow(spot: Spot): ReactNode` instead of title/icon --- browser-extension/src/entrypoints/popup/main.tsx | 3 +-- browser-extension/src/lib/enhancer.ts | 5 ++--- .../src/lib/enhancers/github/githubIssueAddComment.tsx | 10 +--------- .../src/lib/enhancers/github/githubPRAddComment.tsx | 10 +--------- 4 files changed, 5 insertions(+), 23 deletions(-) diff --git a/browser-extension/src/entrypoints/popup/main.tsx b/browser-extension/src/entrypoints/popup/main.tsx index fb2290b..da52da4 100644 --- a/browser-extension/src/entrypoints/popup/main.tsx +++ b/browser-extension/src/entrypoints/popup/main.tsx @@ -68,9 +68,8 @@ function SpotRow({ commentState, onClick }: SpotRowProps) { >
- {enhancer.tableIcon(commentState.spot)}
- {enhancer.tableTitle(commentState.spot)} + {enhancer.tableRow(commentState.spot)}
diff --git a/browser-extension/src/lib/enhancer.ts b/browser-extension/src/lib/enhancer.ts index 8d5f661..06b294f 100644 --- a/browser-extension/src/lib/enhancer.ts +++ b/browser-extension/src/lib/enhancer.ts @@ -41,7 +41,6 @@ export interface CommentEnhancer { * exactly once since pageload before this gets called. */ enhance(textarea: HTMLTextAreaElement, spot: Spot): OverTypeInstance - - tableIcon(spot: Spot): string - tableTitle(spot: Spot): ReactNode + /** Returns a ReactNode which will be displayed in the table row. */ + tableRow(spot: Spot): ReactNode } diff --git a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx index b17b7f7..206d189 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx @@ -55,7 +55,7 @@ export class GitHubIssueAddCommentEnhancer implements CommentEnhancer @@ -64,12 +64,4 @@ export class GitHubIssueAddCommentEnhancer implements CommentEnhancer ) } - - tableIcon(_: GitHubIssueAddCommentSpot): string { - return '🔄' // PR icon TODO: icon urls in /public - } - - buildUrl(spot: GitHubIssueAddCommentSpot): string { - return `https://${spot.domain}/${spot.slug}/issue/${spot.number}` - } } diff --git a/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx index a4ba1bc..1851fc7 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx @@ -59,7 +59,7 @@ export class GitHubPRAddCommentEnhancer implements CommentEnhancer @@ -68,12 +68,4 @@ export class GitHubPRAddCommentEnhancer implements CommentEnhancer ) } - - tableIcon(_: GitHubPRAddCommentSpot): string { - return '🔄' // PR icon TODO: icon urls in /public - } - - buildUrl(spot: GitHubPRAddCommentSpot): string { - return `https://${spot.domain}/${spot.slug}/pull/${spot.number}` - } } From 113586cdc78df279dd525bcf302c0f976b42fca5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 12:08:13 -0700 Subject: [PATCH 005/109] Take full advantage of tailwind v4. --- browser-extension/package.json | 4 +- browser-extension/postcss.config.cjs | 6 --- browser-extension/tailwind.config.cjs | 10 ----- browser-extension/wxt.config.ts | 6 +-- pnpm-lock.yaml | 60 ++++----------------------- 5 files changed, 11 insertions(+), 75 deletions(-) delete mode 100644 browser-extension/postcss.config.cjs delete mode 100644 browser-extension/tailwind.config.cjs diff --git a/browser-extension/package.json b/browser-extension/package.json index 92790c0..4f5b098 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@biomejs/biome": "^2.1.2", "@playwright/test": "^1.46.0", - "@tailwindcss/postcss": "^4.1.13", + "@tailwindcss/vite": "^4.1.13", "@testing-library/jest-dom": "^6.6.4", "@types/express": "^4.17.21", "@types/har-format": "^1.2.16", @@ -26,10 +26,8 @@ "@vitejs/plugin-react": "^5.0.2", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", - "autoprefixer": "^10.4.21", "express": "^4.19.2", "linkedom": "^0.18.12", - "postcss": "^8.5.6", "tailwindcss": "^4.1.13", "tsx": "^4.19.1", "typescript": "^5.8.3", diff --git a/browser-extension/postcss.config.cjs b/browser-extension/postcss.config.cjs deleted file mode 100644 index dc655aa..0000000 --- a/browser-extension/postcss.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - '@tailwindcss/postcss': {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/browser-extension/tailwind.config.cjs b/browser-extension/tailwind.config.cjs deleted file mode 100644 index 08621b4..0000000 --- a/browser-extension/tailwind.config.cjs +++ /dev/null @@ -1,10 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: {}, - }, - plugins: [], -} \ No newline at end of file diff --git a/browser-extension/wxt.config.ts b/browser-extension/wxt.config.ts index 826fb93..6c543af 100644 --- a/browser-extension/wxt.config.ts +++ b/browser-extension/wxt.config.ts @@ -1,13 +1,11 @@ import { defineConfig } from 'wxt' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' import path from 'path' export default defineConfig({ vite: () => ({ - plugins: [react()], - css: { - postcss: path.resolve('./postcss.config.cjs') - }, + plugins: [react(), tailwindcss()], resolve: { alias: { '@': path.resolve('./src') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf482a3..eae8263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,9 +53,9 @@ importers: '@playwright/test': specifier: ^1.46.0 version: 1.55.0 - '@tailwindcss/postcss': + '@tailwindcss/vite': specifier: ^4.1.13 - version: 4.1.13 + version: 4.1.13(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5)) '@testing-library/jest-dom': specifier: ^6.6.4 version: 6.8.0 @@ -77,18 +77,12 @@ importers: '@vitest/ui': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) - autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) express: specifier: ^4.19.2 version: 4.21.2 linkedom: specifier: ^0.18.12 version: 0.18.12 - postcss: - specifier: ^8.5.6 - version: 8.5.6 tailwindcss: specifier: ^4.1.13 version: 4.1.13 @@ -145,10 +139,6 @@ packages: rollup: optional: true - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -900,8 +890,10 @@ packages: resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.13': - resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==} + '@tailwindcss/vite@4.1.13': + resolution: {integrity: sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 '@testing-library/jest-dom@6.8.0': resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==} @@ -1142,13 +1134,6 @@ packages: atomically@2.0.3: resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1744,9 +1729,6 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -2468,10 +2450,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -2654,9 +2632,6 @@ packages: resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==} engines: {node: '>= 10.12'} - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -3501,8 +3476,6 @@ snapshots: optionalDependencies: rollup: 4.50.1 - '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -4052,13 +4025,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 - '@tailwindcss/postcss@4.1.13': + '@tailwindcss/vite@4.1.13(vite@7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5))': dependencies: - '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.1.13 '@tailwindcss/oxide': 4.1.13 - postcss: 8.5.6 tailwindcss: 4.1.13 + vite: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) '@testing-library/jest-dom@6.8.0': dependencies: @@ -4343,16 +4315,6 @@ snapshots: stubborn-fs: 1.2.5 when-exit: 2.1.4 - autoprefixer@10.4.21(postcss@8.5.6): - dependencies: - browserslist: 4.25.4 - caniuse-lite: 1.0.30001741 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-value-parser: 4.2.0 - balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -4985,8 +4947,6 @@ snapshots: forwarded@0.2.0: {} - fraction.js@4.3.7: {} - fresh@0.5.2: {} fs-extra@11.3.1: @@ -5669,8 +5629,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -5878,8 +5836,6 @@ snapshots: transitivePeerDependencies: - supports-color - postcss-value-parser@4.2.0: {} - postcss@8.5.6: dependencies: nanoid: 3.3.11 From 4ea95c03888acdb67a982b9758b1908700e555b0 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 13:35:28 -0700 Subject: [PATCH 006/109] Update README. --- browser-extension/README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/browser-extension/README.md b/browser-extension/README.md index cb2641f..543713b 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -31,17 +31,26 @@ This is a [WXT](https://wxt.dev/)-based browser extension that - finds `textarea` components and decorates them with [overtype](https://overtype.dev/) and [highlight.js](https://highlightjs.org/) - stores unposted comment drafts, and makes them easy to find via the extension popup +### Tech Stack + +- **Framework**: [WXT](https://wxt.dev/) for browser extension development +- **UI**: React with TypeScript JSX +- **Styling**: Tailwind CSS v4 (with first-party Vite plugin) +- **Components**: shadcn/ui for table components +- **Editor Enhancement**: [Overtype](https://overtype.dev/) with syntax highlighting +- **Build**: Vite with React plugin + ### Entry points - `src/entrypoints/content.ts` - injected into every webpage - `src/entrypoints/background.ts` - service worker that manages state and handles messages -- `src/entrypoints/popup` - html/css/ts which opens when the extension's button gets clicked +- `src/entrypoints/popup` - React-based popup (html/css/tsx) with shadcn/ui table components ```mermaid graph TD Content[Content Script
content.ts] Background[Background Script
background.ts] - Popup[Popup Script
popup/main.ts] + Popup[Popup Script
popup/main.tsx] Content -->|ENHANCED/DESTROYED
CommentEvent| Background Popup -->|GET_OPEN_SPOTS
SWITCH_TO_TAB| Background @@ -62,11 +71,11 @@ graph TD ### Architecture -Every time a `textarea` shows up on a page, on initial load or later on, it gets passed to a list of `CommentEnhancer`s. Each one gets a turn to say "I can enhance this box!". They show that they can enhance it by returning a [`CommentSpot`, `Overtype`]. +Every time a `textarea` shows up on a page, on initial load or later on, it gets passed to a list of `CommentEnhancer`s. Each one gets a turn to say "I can enhance this box!". They show that they can enhance it by returning something non-null in the method `tryToEnhance(textarea: HTMLTextAreaElement): Spot | null`. Later on, that same `Spot` data will be used by the `tableRow(spot: Spot): ReactNode` method to create React components for rich formatting in the popup table. -Those values get bundled up with the `HTMLTextAreaElement` itself into an `EnhancedTextarea`, which gets added to the `TextareaRegistry`. At some interval, draft edits will get saved by the browser extension (TODO). +Those `Spot` values get bundled up with the `HTMLTextAreaElement` itself into an `EnhancedTextarea`, which gets added to the `TextareaRegistry`. At some interval, draft edits get saved by the browser extension. -When the `textarea` gets removed from the page, the `TextareaRegistry` is notified so that the `CommentSpot` can be marked as abandoned or submitted as appropriate (TODO). +When the `textarea` gets removed from the page, the `TextareaRegistry` is notified so that the `CommentSpot` can be marked as abandoned or submitted as appropriate. ## Testing From 49bc9b31f1de009a7049b4afd47b4c6af0a6bd05 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 13:43:58 -0700 Subject: [PATCH 007/109] Propery initialize shadcn/ui --- browser-extension/components.json | 21 ++++++ browser-extension/package.json | 1 + .../src/components/ui/button.tsx | 56 +++++++++++++++ .../src/entrypoints/background.ts | 6 +- .../src/entrypoints/popup/main.tsx | 8 +-- browser-extension/src/styles/globals.css | 69 +++++++++++++++++++ pnpm-lock.yaml | 34 +++++++++ 7 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 browser-extension/components.json create mode 100644 browser-extension/src/components/ui/button.tsx create mode 100644 browser-extension/src/styles/globals.css diff --git a/browser-extension/components.json b/browser-extension/components.json new file mode 100644 index 0000000..cd33c15 --- /dev/null +++ b/browser-extension/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/styles/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/browser-extension/package.json b/browser-extension/package.json index 4f5b098..5b33068 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,6 +1,7 @@ { "author": "DiffPlug", "dependencies": { + "@radix-ui/react-slot": "^1.2.3", "@types/react": "^19.1.12", "@types/react-dom": "^19.1.9", "@wxt-dev/webextension-polyfill": "^1.0.0", diff --git a/browser-extension/src/components/ui/button.tsx b/browser-extension/src/components/ui/button.tsx new file mode 100644 index 0000000..36496a2 --- /dev/null +++ b/browser-extension/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/browser-extension/src/entrypoints/background.ts b/browser-extension/src/entrypoints/background.ts index 6cb8e13..3dcdf7d 100644 --- a/browser-extension/src/entrypoints/background.ts +++ b/browser-extension/src/entrypoints/background.ts @@ -1,12 +1,12 @@ -import type { CommentDraft, CommentEvent, CommentSpot } from '../lib/enhancer' -import type { GetOpenSpotsResponse, ToBackgroundMessage } from '../lib/messages' +import type { CommentDraft, CommentEvent, CommentSpot } from '@/lib/enhancer' +import type { GetOpenSpotsResponse, ToBackgroundMessage } from '@/lib/messages' import { CLOSE_MESSAGE_PORT, isContentToBackgroundMessage, isGetOpenSpotsMessage, isSwitchToTabMessage, KEEP_PORT_OPEN, -} from '../lib/messages' +} from '@/lib/messages' export interface Tab { tabId: number diff --git a/browser-extension/src/entrypoints/popup/main.tsx b/browser-extension/src/entrypoints/popup/main.tsx index da52da4..dff0a45 100644 --- a/browser-extension/src/entrypoints/popup/main.tsx +++ b/browser-extension/src/entrypoints/popup/main.tsx @@ -10,14 +10,14 @@ import { TableRow, } from '@/components/ui/table' import { cn } from '@/lib/utils' -import { logger } from '../../lib/logger' +import { logger } from '@/lib/logger' import type { GetOpenSpotsMessage, GetOpenSpotsResponse, SwitchToTabMessage, -} from '../../lib/messages' -import { EnhancerRegistry } from '../../lib/registries' -import type { CommentState } from '../background' +} from '@/lib/messages' +import { EnhancerRegistry } from '@/lib/registries' +import type { CommentState } from '@/entrypoints/background' const enhancers = new EnhancerRegistry() diff --git a/browser-extension/src/styles/globals.css b/browser-extension/src/styles/globals.css new file mode 100644 index 0000000..494f343 --- /dev/null +++ b/browser-extension/src/styles/globals.css @@ -0,0 +1,69 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eae8263..1671ad6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: browser-extension: dependencies: + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.1.12)(react@19.1.1) '@types/react': specifier: ^19.1.12 version: 19.1.12 @@ -697,6 +700,24 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rolldown/pluginutils@1.0.0-beta.34': resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} @@ -3896,6 +3917,19 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.12)(react@19.1.1)': + dependencies: + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.12 + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.12)(react@19.1.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.12 + '@rolldown/pluginutils@1.0.0-beta.34': {} '@rollup/rollup-android-arm-eabi@4.50.1': From 1274c11933acb2f6f577e04a6d63dfa79d3833a9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 13:48:35 -0700 Subject: [PATCH 008/109] biome:fix --- .../src/components/ui/button.tsx | 59 ++++++++----------- browser-extension/src/entrypoints/content.ts | 10 ++-- .../src/entrypoints/popup/main.tsx | 10 +--- .../github/githubIssueAddComment.tsx | 4 +- .../enhancers/github/githubPRAddComment.tsx | 4 +- browser-extension/src/styles/globals.css | 2 +- 6 files changed, 39 insertions(+), 50 deletions(-) diff --git a/browser-extension/src/components/ui/button.tsx b/browser-extension/src/components/ui/button.tsx index 36496a2..80f6691 100644 --- a/browser-extension/src/components/ui/button.tsx +++ b/browser-extension/src/components/ui/button.tsx @@ -1,36 +1,33 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { + defaultVariants: { + size: 'default', + variant: 'default', + }, variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", + default: 'h-10 px-4 py-2', + icon: 'h-10 w-10', + lg: 'h-11 rounded-md px-8', + sm: 'h-9 rounded-md px-3', + }, + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', }, }, - defaultVariants: { - variant: "default", - size: "default", - }, - } + }, ) export interface ButtonProps @@ -41,16 +38,12 @@ export interface ButtonProps const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' return ( - + ) - } + }, ) -Button.displayName = "Button" +Button.displayName = 'Button' export { Button, buttonVariants } diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 83de338..c005929 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,8 +1,8 @@ -import { CONFIG, type ModeType } from '../lib/config' -import type { CommentEvent, CommentSpot } from '../lib/enhancer' -import { logger } from '../lib/logger' -import { EnhancerRegistry, TextareaRegistry } from '../lib/registries' -import { githubPrNewCommentContentScript } from '../playgrounds/github-playground' +import { CONFIG, type ModeType } from '@/lib/config' +import type { CommentEvent, CommentSpot } from '@/lib/enhancer' +import { logger } from '@/lib/logger' +import { EnhancerRegistry, TextareaRegistry } from '@/lib/registries' +import { githubPrNewCommentContentScript } from '@/playgrounds/github-playground' const enhancers = new EnhancerRegistry() const enhancedTextareas = new TextareaRegistry() diff --git a/browser-extension/src/entrypoints/popup/main.tsx b/browser-extension/src/entrypoints/popup/main.tsx index dff0a45..6ddbb2b 100644 --- a/browser-extension/src/entrypoints/popup/main.tsx +++ b/browser-extension/src/entrypoints/popup/main.tsx @@ -9,15 +9,11 @@ import { TableHeader, TableRow, } from '@/components/ui/table' -import { cn } from '@/lib/utils' +import type { CommentState } from '@/entrypoints/background' import { logger } from '@/lib/logger' -import type { - GetOpenSpotsMessage, - GetOpenSpotsResponse, - SwitchToTabMessage, -} from '@/lib/messages' +import type { GetOpenSpotsMessage, GetOpenSpotsResponse, SwitchToTabMessage } from '@/lib/messages' import { EnhancerRegistry } from '@/lib/registries' -import type { CommentState } from '@/entrypoints/background' +import { cn } from '@/lib/utils' const enhancers = new EnhancerRegistry() diff --git a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx index 206d189..84d4cf4 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx @@ -1,7 +1,7 @@ import OverType, { type OverTypeInstance } from 'overtype' import type React from 'react' -import type { CommentEnhancer, CommentSpot } from '../../enhancer' -import { logger } from '../../logger' +import type { CommentEnhancer, CommentSpot } from '@/lib/enhancer' +import { logger } from '@/lib/logger' import { modifyDOM } from '../modifyDOM' import { githubHighlighter } from './githubHighlighter' diff --git a/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx index 1851fc7..d228196 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx @@ -1,7 +1,7 @@ import OverType, { type OverTypeInstance } from 'overtype' import type React from 'react' -import type { CommentEnhancer, CommentSpot } from '../../enhancer' -import { logger } from '../../logger' +import type { CommentEnhancer, CommentSpot } from '@/lib/enhancer' +import { logger } from '@/lib/logger' import { modifyDOM } from '../modifyDOM' import { githubHighlighter } from './githubHighlighter' diff --git a/browser-extension/src/styles/globals.css b/browser-extension/src/styles/globals.css index 494f343..3094824 100644 --- a/browser-extension/src/styles/globals.css +++ b/browser-extension/src/styles/globals.css @@ -66,4 +66,4 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} From 2a72e6f75ede2d77dac7d9c00dd02b837b4344a1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 14:35:54 -0700 Subject: [PATCH 009/109] Add a playground for the table with hotreload. --- browser-extension/package.json | 1 + .../github/githubIssueAddComment.tsx | 2 +- .../enhancers/github/githubPRAddComment.tsx | 2 +- .../tests/lib/enhancers/github.test.ts | 37 +++++++++- .../tests/playground/TablePlayground.tsx | 74 +++++++++++++++++++ browser-extension/tests/playground/index.html | 13 ++++ browser-extension/tests/playground/main.tsx | 28 +++++++ .../tests/playground/mockData.tsx | 43 +++++++++++ browser-extension/tests/playground/style.css | 16 ++++ browser-extension/vite.playground.config.ts | 26 +++++++ 10 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 browser-extension/tests/playground/TablePlayground.tsx create mode 100644 browser-extension/tests/playground/index.html create mode 100644 browser-extension/tests/playground/main.tsx create mode 100644 browser-extension/tests/playground/mockData.tsx create mode 100644 browser-extension/tests/playground/style.css create mode 100644 browser-extension/vite.playground.config.ts diff --git a/browser-extension/package.json b/browser-extension/package.json index 5b33068..4d7012a 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -59,6 +59,7 @@ "dev:firefox": "wxt -b firefox", "postinstall": "wxt prepare", "test": "vitest run", + "playground": "vite --config vite.playground.config.ts", "har:record": "tsx tests/har-record.ts", "har:view": "tsx tests/har-view.ts" }, diff --git a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx index 84d4cf4..b35f8f9 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx @@ -5,7 +5,7 @@ import { logger } from '@/lib/logger' import { modifyDOM } from '../modifyDOM' import { githubHighlighter } from './githubHighlighter' -interface GitHubIssueAddCommentSpot extends CommentSpot { +export interface GitHubIssueAddCommentSpot extends CommentSpot { type: 'GH_ISSUE_ADD_COMMENT' domain: string slug: string // owner/repo diff --git a/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx index d228196..0b70cb4 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx @@ -5,7 +5,7 @@ import { logger } from '@/lib/logger' import { modifyDOM } from '../modifyDOM' import { githubHighlighter } from './githubHighlighter' -interface GitHubPRAddCommentSpot extends CommentSpot { +export interface GitHubPRAddCommentSpot extends CommentSpot { type: 'GH_PR_ADD_COMMENT' // Override to narrow from string to specific union domain: string slug: string // owner/repo diff --git a/browser-extension/tests/lib/enhancers/github.test.ts b/browser-extension/tests/lib/enhancers/github.test.ts index 3952e81..c119651 100644 --- a/browser-extension/tests/lib/enhancers/github.test.ts +++ b/browser-extension/tests/lib/enhancers/github.test.ts @@ -11,7 +11,8 @@ describe('github', () => { const textareas = document.querySelectorAll('textarea') expect(textareas.length).toBe(2) expect(enhancers.tryToEnhance(textareas[0]!)).toBeNull() - expect(enhancers.tryToEnhance(textareas[1]!)?.spot).toMatchInlineSnapshot(` + const enhancedTextarea = enhancers.tryToEnhance(textareas[1]!) + expect(enhancedTextarea?.spot).toMatchInlineSnapshot(` { "domain": "github.com", "number": 517, @@ -20,12 +21,28 @@ describe('github', () => { "unique_key": "github.com:diffplug/selfie:517", } `) + expect(enhancedTextarea?.enhancer.tableRow(enhancedTextarea.spot)).toMatchInlineSnapshot(` + + + diffplug/selfie + + + PR # + 517 + + + `) }) usingHar('gh_issue').it('should create the correct spot object', async () => { const enhancers = new EnhancerRegistry() const textareas = document.querySelectorAll('textarea') expect(textareas.length).toBe(1) - expect(enhancers.tryToEnhance(textareas[0]!)?.spot).toMatchInlineSnapshot(` + const enhancedTextarea = enhancers.tryToEnhance(textareas[0]!) + expect(enhancedTextarea?.spot).toMatchInlineSnapshot(` { "domain": "github.com", "number": 523, @@ -34,5 +51,21 @@ describe('github', () => { "unique_key": "github.com:diffplug/selfie:523", } `) + // Test the tableRow method + expect(enhancedTextarea?.enhancer.tableRow(enhancedTextarea.spot)).toMatchInlineSnapshot(` + + + diffplug/selfie + + + Issue # + 523 + + + `) }) }) diff --git a/browser-extension/tests/playground/TablePlayground.tsx b/browser-extension/tests/playground/TablePlayground.tsx new file mode 100644 index 0000000..167e297 --- /dev/null +++ b/browser-extension/tests/playground/TablePlayground.tsx @@ -0,0 +1,74 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import type { CommentState } from '@/entrypoints/background' +import { cn } from '@/lib/utils' +import { enhancerRegistry, sampleSpots } from './mockData' + +interface SpotRowProps { + commentState: CommentState + onClick: () => void +} + +function SpotRow({ commentState, onClick }: SpotRowProps) { + const enhancer = enhancerRegistry.enhancerFor(commentState.spot) + + if (!enhancer) { + return ( + + +
Unknown spot type: {commentState.spot.type}
+
+
+ ) + } + + return ( + + {enhancer.tableRow(commentState.spot)} + + ) +} + +export function TablePlayground() { + const handleSpotClick = (spot: CommentState) => { + alert(`Clicked: ${spot.spot.type}\nTab: ${spot.tab.tabId}`) + } + + return ( +
+
+

Comment Spots

+

Click on any row to simulate tab switching

+
+ + + + + Spot Details + + + + {sampleSpots.map((spot) => ( + handleSpotClick(spot)} + /> + ))} + +
+
+ ) +} diff --git a/browser-extension/tests/playground/index.html b/browser-extension/tests/playground/index.html new file mode 100644 index 0000000..09a40b6 --- /dev/null +++ b/browser-extension/tests/playground/index.html @@ -0,0 +1,13 @@ + + + + + + + Table Playground + + +
+ + + \ No newline at end of file diff --git a/browser-extension/tests/playground/main.tsx b/browser-extension/tests/playground/main.tsx new file mode 100644 index 0000000..dc0bb62 --- /dev/null +++ b/browser-extension/tests/playground/main.tsx @@ -0,0 +1,28 @@ +import { createRoot } from 'react-dom/client' +import './style.css' +import { TablePlayground } from './TablePlayground' + +const root = createRoot(document.getElementById('root')!) +root.render( +
+
+
+

Table Playground

+

+ Testing table rendering with real enhancers and sample data. +

+
+ + + +
+

Development Notes

+
    +
  • • Hot reload is active - changes to components update instantly
  • +
  • • Uses real enhancers from the browser extension
  • +
  • • Click rows to test interaction behavior
  • +
+
+
+
, +) diff --git a/browser-extension/tests/playground/mockData.tsx b/browser-extension/tests/playground/mockData.tsx new file mode 100644 index 0000000..ca67fec --- /dev/null +++ b/browser-extension/tests/playground/mockData.tsx @@ -0,0 +1,43 @@ +import type { CommentState } from '@/entrypoints/background' +import type { CommentSpot } from '@/lib/enhancer' +import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' +import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' +import { EnhancerRegistry } from '@/lib/registries' + +const gh_pr: GitHubPRAddCommentSpot = { + domain: 'github.com', + number: 517, + slug: 'diffplug/selfie', + type: 'GH_PR_ADD_COMMENT', + unique_key: 'github.com:diffplug/selfie:517', +} +const gh_issue: GitHubIssueAddCommentSpot = { + domain: 'github.com', + number: 523, + slug: 'diffplug/selfie', + type: 'GH_ISSUE_ADD_COMMENT', + unique_key: 'github.com:diffplug/selfie:523', +} + +const spots: CommentSpot[] = [gh_pr, gh_issue] + +export const sampleSpots: CommentState[] = spots.map((spot) => { + const state: CommentState = { + drafts: [ + [ + 0, + { + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + ], + ], + spot, + tab: { + tabId: 123, + windowId: 456, + }, + } + return state +}) + +export const enhancerRegistry = new EnhancerRegistry() diff --git a/browser-extension/tests/playground/style.css b/browser-extension/tests/playground/style.css new file mode 100644 index 0000000..c62bebb --- /dev/null +++ b/browser-extension/tests/playground/style.css @@ -0,0 +1,16 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + margin: 0; + padding: 2rem; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: #f8fafc; + min-height: 100vh; +} + +#root { + max-width: 1200px; + margin: 0 auto; +} diff --git a/browser-extension/vite.playground.config.ts b/browser-extension/vite.playground.config.ts new file mode 100644 index 0000000..37888e5 --- /dev/null +++ b/browser-extension/vite.playground.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import path from 'path' + +export default defineConfig({ + plugins: [ + react(), + tailwindcss() as any + ], + resolve: { + alias: { + '@': path.resolve('./src') + } + }, + root: 'tests/playground', + server: { + port: 3002, + open: true, + host: true + }, + build: { + outDir: '../../dist-playground', + emptyOutDir: true + } +}) \ No newline at end of file From 7ba5e88ed5091c5559895eeb21e4502e9ee2e660 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 15:50:44 -0700 Subject: [PATCH 010/109] Improve commonality between prod and test. --- browser-extension/src/components/SpotRow.tsx | 40 +++++++++ .../src/components/SpotTable.tsx | 78 ++++++++++++++++++ .../src/entrypoints/popup/main.tsx | 77 ++++------------- .../tests/playground/TablePlayground.tsx | 82 ++++--------------- .../tests/playground/mockData.tsx | 3 - 5 files changed, 149 insertions(+), 131 deletions(-) create mode 100644 browser-extension/src/components/SpotRow.tsx create mode 100644 browser-extension/src/components/SpotTable.tsx diff --git a/browser-extension/src/components/SpotRow.tsx b/browser-extension/src/components/SpotRow.tsx new file mode 100644 index 0000000..a394901 --- /dev/null +++ b/browser-extension/src/components/SpotRow.tsx @@ -0,0 +1,40 @@ +import { TableCell, TableRow } from '@/components/ui/table' +import type { CommentState } from '@/entrypoints/background' +import type { EnhancerRegistry } from '@/lib/registries' +import { cn } from '@/lib/utils' + +interface SpotRowProps { + commentState: CommentState + enhancerRegistry: EnhancerRegistry + onClick: () => void + className?: string + cellClassName?: string + errorClassName?: string +} + +export function SpotRow({ + commentState, + enhancerRegistry, + onClick, + className, + cellClassName = 'p-3', + errorClassName = 'text-red-500', +}: SpotRowProps) { + const enhancer = enhancerRegistry.enhancerFor(commentState.spot) + + if (!enhancer) { + return ( + + +
Unknown spot type: {commentState.spot.type}
+
+
+ ) + } + + return ( + + {enhancer.tableRow(commentState.spot)} + + ) +} diff --git a/browser-extension/src/components/SpotTable.tsx b/browser-extension/src/components/SpotTable.tsx new file mode 100644 index 0000000..33e26e7 --- /dev/null +++ b/browser-extension/src/components/SpotTable.tsx @@ -0,0 +1,78 @@ +import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import type { CommentState } from '@/entrypoints/background' +import type { EnhancerRegistry } from '@/lib/registries' +import { SpotRow } from './SpotRow' + +interface SpotTableProps { + spots: CommentState[] + enhancerRegistry: EnhancerRegistry + onSpotClick: (spot: CommentState) => void + title?: string + description?: string + headerText?: string + className?: string + headerClassName?: string + rowClassName?: string + cellClassName?: string + emptyStateMessage?: string + showHeader?: boolean +} + +export function SpotTable({ + spots, + enhancerRegistry, + onSpotClick, + title, + description, + headerText = 'Comment Spots', + className, + headerClassName = 'p-3 font-medium text-muted-foreground', + rowClassName, + cellClassName, + emptyStateMessage = 'No comment spots available', + showHeader = true, +}: SpotTableProps) { + if (spots.length === 0) { + return
{emptyStateMessage}
+ } + + const tableContent = ( + + {showHeader && ( + + + {headerText} + + + )} + + {spots.map((spot) => ( + onSpotClick(spot)} + className={rowClassName || ''} + cellClassName={cellClassName || 'p-3'} + /> + ))} + +
+ ) + + if (title || description) { + return ( +
+ {(title || description) && ( +
+ {title &&

{title}

} + {description &&

{description}

} +
+ )} + {tableContent} +
+ ) + } + + return
{tableContent}
+} diff --git a/browser-extension/src/entrypoints/popup/main.tsx b/browser-extension/src/entrypoints/popup/main.tsx index 6ddbb2b..fa1cea2 100644 --- a/browser-extension/src/entrypoints/popup/main.tsx +++ b/browser-extension/src/entrypoints/popup/main.tsx @@ -1,21 +1,11 @@ import './style.css' import React from 'react' import { createRoot } from 'react-dom/client' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' +import { SpotTable } from '@/components/SpotTable' import type { CommentState } from '@/entrypoints/background' import { logger } from '@/lib/logger' import type { GetOpenSpotsMessage, GetOpenSpotsResponse, SwitchToTabMessage } from '@/lib/messages' import { EnhancerRegistry } from '@/lib/registries' -import { cn } from '@/lib/utils' - -const enhancers = new EnhancerRegistry() async function getOpenSpots(): Promise { logger.debug('Sending message to background script...') @@ -40,38 +30,7 @@ function switchToTab(tabId: number, windowId: number): void { window.close() } -interface SpotRowProps { - commentState: CommentState - onClick: () => void -} - -function SpotRow({ commentState, onClick }: SpotRowProps) { - const enhancer = enhancers.enhancerFor(commentState.spot) - - if (!enhancer) { - logger.error('No enhancer found for:', commentState.spot) - logger.error('Only have enhancers for:', enhancers.byType) - return null - } - - return ( - - -
-
- {enhancer.tableRow(commentState.spot)} -
-
-
-
- ) -} +const enhancers = new EnhancerRegistry() function PopupApp() { const [spots, setSpots] = React.useState([]) @@ -96,10 +55,8 @@ function PopupApp() { return
Loading...
} - if (spots.length === 0) { - return ( -
No open comment spots
- ) + const handleSpotClick = (spot: CommentState) => { + switchToTab(spot.tab.tabId, spot.tab.windowId) } return ( @@ -107,22 +64,16 @@ function PopupApp() {

Open Comment Spots

- - - - Comment Spots - - - - {spots.map((spot) => ( - switchToTab(spot.tab.tabId, spot.tab.windowId)} - /> - ))} - -
+
) diff --git a/browser-extension/tests/playground/TablePlayground.tsx b/browser-extension/tests/playground/TablePlayground.tsx index 167e297..089110a 100644 --- a/browser-extension/tests/playground/TablePlayground.tsx +++ b/browser-extension/tests/playground/TablePlayground.tsx @@ -1,74 +1,26 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' +import { SpotTable } from '@/components/SpotTable' import type { CommentState } from '@/entrypoints/background' -import { cn } from '@/lib/utils' -import { enhancerRegistry, sampleSpots } from './mockData' - -interface SpotRowProps { - commentState: CommentState - onClick: () => void -} - -function SpotRow({ commentState, onClick }: SpotRowProps) { - const enhancer = enhancerRegistry.enhancerFor(commentState.spot) - - if (!enhancer) { - return ( - - -
Unknown spot type: {commentState.spot.type}
-
-
- ) - } - - return ( - - {enhancer.tableRow(commentState.spot)} - - ) -} +import { EnhancerRegistry } from '@/lib/registries' +import { sampleSpots } from './mockData' export function TablePlayground() { const handleSpotClick = (spot: CommentState) => { alert(`Clicked: ${spot.spot.type}\nTab: ${spot.tab.tabId}`) } - + const enhancers = new EnhancerRegistry() return ( -
-
-

Comment Spots

-

Click on any row to simulate tab switching

-
- - - - - Spot Details - - - - {sampleSpots.map((spot) => ( - handleSpotClick(spot)} - /> - ))} - -
-
+ ) } diff --git a/browser-extension/tests/playground/mockData.tsx b/browser-extension/tests/playground/mockData.tsx index ca67fec..57fd727 100644 --- a/browser-extension/tests/playground/mockData.tsx +++ b/browser-extension/tests/playground/mockData.tsx @@ -2,7 +2,6 @@ import type { CommentState } from '@/entrypoints/background' import type { CommentSpot } from '@/lib/enhancer' import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' -import { EnhancerRegistry } from '@/lib/registries' const gh_pr: GitHubPRAddCommentSpot = { domain: 'github.com', @@ -39,5 +38,3 @@ export const sampleSpots: CommentState[] = spots.map((spot) => { } return state }) - -export const enhancerRegistry = new EnhancerRegistry() From da006a39e2efe6d72fccdef0556e7fbfb77fe8a2 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 15:57:42 -0700 Subject: [PATCH 011/109] Renames for easier command-palette navigation. --- browser-extension/src/entrypoints/popup/index.html | 2 +- .../src/entrypoints/popup/{main.tsx => popup.tsx} | 0 .../{TablePlayground.tsx => PopupPlayground.tsx} | 4 ++-- .../tests/playground/{main.tsx => playground.tsx} | 12 +++++++----- .../playground/{mockData.tsx => playgroundData.tsx} | 0 5 files changed, 10 insertions(+), 8 deletions(-) rename browser-extension/src/entrypoints/popup/{main.tsx => popup.tsx} (100%) rename browser-extension/tests/playground/{TablePlayground.tsx => PopupPlayground.tsx} (91%) rename browser-extension/tests/playground/{main.tsx => playground.tsx} (71%) rename browser-extension/tests/playground/{mockData.tsx => playgroundData.tsx} (100%) diff --git a/browser-extension/src/entrypoints/popup/index.html b/browser-extension/src/entrypoints/popup/index.html index 8a09020..66aa778 100644 --- a/browser-extension/src/entrypoints/popup/index.html +++ b/browser-extension/src/entrypoints/popup/index.html @@ -8,6 +8,6 @@
- + \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/main.tsx b/browser-extension/src/entrypoints/popup/popup.tsx similarity index 100% rename from browser-extension/src/entrypoints/popup/main.tsx rename to browser-extension/src/entrypoints/popup/popup.tsx diff --git a/browser-extension/tests/playground/TablePlayground.tsx b/browser-extension/tests/playground/PopupPlayground.tsx similarity index 91% rename from browser-extension/tests/playground/TablePlayground.tsx rename to browser-extension/tests/playground/PopupPlayground.tsx index 089110a..4b5bdc0 100644 --- a/browser-extension/tests/playground/TablePlayground.tsx +++ b/browser-extension/tests/playground/PopupPlayground.tsx @@ -1,9 +1,9 @@ import { SpotTable } from '@/components/SpotTable' import type { CommentState } from '@/entrypoints/background' import { EnhancerRegistry } from '@/lib/registries' -import { sampleSpots } from './mockData' +import { sampleSpots } from './playgroundData' -export function TablePlayground() { +export function PopupPlayground() { const handleSpotClick = (spot: CommentState) => { alert(`Clicked: ${spot.spot.type}\nTab: ${spot.tab.tabId}`) } diff --git a/browser-extension/tests/playground/main.tsx b/browser-extension/tests/playground/playground.tsx similarity index 71% rename from browser-extension/tests/playground/main.tsx rename to browser-extension/tests/playground/playground.tsx index dc0bb62..7a97299 100644 --- a/browser-extension/tests/playground/main.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -1,6 +1,6 @@ import { createRoot } from 'react-dom/client' import './style.css' -import { TablePlayground } from './TablePlayground' +import { PopupPlayground } from './PopupPlayground' const root = createRoot(document.getElementById('root')!) root.render( @@ -13,14 +13,16 @@ root.render(

- +

Development Notes

    -
  • • Hot reload is active - changes to components update instantly
  • -
  • • Uses real enhancers from the browser extension
  • -
  • • Click rows to test interaction behavior
  • +
  • Hot reload is active - changes to components update instantly
  • +
  • Uses real enhancers from the browser extension
  • +
  • + Sample data comes from playgroundData.tsx +
diff --git a/browser-extension/tests/playground/mockData.tsx b/browser-extension/tests/playground/playgroundData.tsx similarity index 100% rename from browser-extension/tests/playground/mockData.tsx rename to browser-extension/tests/playground/playgroundData.tsx From 4ed332c5e268a17768f5d2cedde1657ac1e2f03f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 17:09:43 -0700 Subject: [PATCH 012/109] We ned postcss at devtime. --- browser-extension/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/browser-extension/package.json b/browser-extension/package.json index 4d7012a..00bb9a5 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -29,6 +29,7 @@ "@vitest/ui": "^3.2.4", "express": "^4.19.2", "linkedom": "^0.18.12", + "postcss": "^8.5.6", "tailwindcss": "^4.1.13", "tsx": "^4.19.1", "typescript": "^5.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1671ad6..50f0e46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: linkedom: specifier: ^0.18.12 version: 0.18.12 + postcss: + specifier: ^8.5.6 + version: 8.5.6 tailwindcss: specifier: ^4.1.13 version: 4.1.13 From 44fdbb555c7a4adc634e871bd73e7859002a3ed2 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 17:11:47 -0700 Subject: [PATCH 013/109] playground is alive --- .../src/entrypoints/popup/style.css | 11 ++----- browser-extension/src/styles/popup-frame.css | 9 ++++++ .../tests/playground/PopupPlayground.tsx | 31 +++++++++++-------- browser-extension/tests/playground/index.html | 2 +- .../tests/playground/playground.tsx | 21 ++++++++----- browser-extension/tests/playground/style.css | 20 +++++++++--- 6 files changed, 59 insertions(+), 35 deletions(-) create mode 100644 browser-extension/src/styles/popup-frame.css diff --git a/browser-extension/src/entrypoints/popup/style.css b/browser-extension/src/entrypoints/popup/style.css index b773d69..8e9d480 100644 --- a/browser-extension/src/entrypoints/popup/style.css +++ b/browser-extension/src/entrypoints/popup/style.css @@ -1,12 +1,5 @@ +@import url("../../styles/popup-frame.css"); + @tailwind base; @tailwind components; @tailwind utilities; - -body { - width: 300px; - padding: 15px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - font-size: 14px; - line-height: 1.4; - margin: 0; -} diff --git a/browser-extension/src/styles/popup-frame.css b/browser-extension/src/styles/popup-frame.css new file mode 100644 index 0000000..b03d1a4 --- /dev/null +++ b/browser-extension/src/styles/popup-frame.css @@ -0,0 +1,9 @@ +/* Popup window frame styles */ +body { + width: 300px; + padding: 15px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + line-height: 1.4; + margin: 0; +} diff --git a/browser-extension/tests/playground/PopupPlayground.tsx b/browser-extension/tests/playground/PopupPlayground.tsx index 4b5bdc0..8a188f7 100644 --- a/browser-extension/tests/playground/PopupPlayground.tsx +++ b/browser-extension/tests/playground/PopupPlayground.tsx @@ -7,20 +7,25 @@ export function PopupPlayground() { const handleSpotClick = (spot: CommentState) => { alert(`Clicked: ${spot.spot.type}\nTab: ${spot.tab.tabId}`) } + const enhancers = new EnhancerRegistry() + return ( - +
+

Open Comment Spots

+ +
+ +
+
) } diff --git a/browser-extension/tests/playground/index.html b/browser-extension/tests/playground/index.html index 09a40b6..0138cf2 100644 --- a/browser-extension/tests/playground/index.html +++ b/browser-extension/tests/playground/index.html @@ -8,6 +8,6 @@
- + \ No newline at end of file diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 7a97299..317cfb2 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -1,28 +1,33 @@ import { createRoot } from 'react-dom/client' +import '@/entrypoints/popup/style.css' import './style.css' import { PopupPlayground } from './PopupPlayground' const root = createRoot(document.getElementById('root')!) root.render(
-
+
-

Table Playground

+

Popup Simulator

- Testing table rendering with real enhancers and sample data. + This shows exactly how the table appears in the browser popup (300px width).

- +
+ +
-
+

Development Notes

    -
  • Hot reload is active - changes to components update instantly
  • -
  • Uses real enhancers from the browser extension
  • - Sample data comes from playgroundData.tsx + The popup frame above matches the exact 300px width of the browser extension popup +
  • +
  • + Any changes to popup/style.css will automatically update here
  • +
  • Hot reload is active for instant updates
diff --git a/browser-extension/tests/playground/style.css b/browser-extension/tests/playground/style.css index c62bebb..025d849 100644 --- a/browser-extension/tests/playground/style.css +++ b/browser-extension/tests/playground/style.css @@ -1,16 +1,28 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +/* Playground-specific styles - popup styles are imported via popup/style.css */ +/* Override body styles for playground layout */ body { margin: 0; padding: 2rem; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f8fafc; min-height: 100vh; + width: auto; /* Override popup's fixed width for playground */ } #root { max-width: 1200px; margin: 0 auto; } + +/* Popup simulator frame */ +.popup-frame { + width: 300px; + padding: 15px; + font-size: 14px; + line-height: 1.4; + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); + margin: 0 auto; +} From 9e9744796ee23280a8ba9567b63e62765fe8b00c Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 17:34:18 -0700 Subject: [PATCH 014/109] Get the playground to match the popup exactly. --- browser-extension/tests/playground/playground.tsx | 2 +- browser-extension/tests/playground/style.css | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 317cfb2..cac9281 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -6,7 +6,7 @@ import { PopupPlayground } from './PopupPlayground' const root = createRoot(document.getElementById('root')!) root.render(
-
+

Popup Simulator

diff --git a/browser-extension/tests/playground/style.css b/browser-extension/tests/playground/style.css index 025d849..b1ada89 100644 --- a/browser-extension/tests/playground/style.css +++ b/browser-extension/tests/playground/style.css @@ -11,7 +11,7 @@ body { #root { max-width: 1200px; - margin: 0 auto; + margin: 0; } /* Popup simulator frame */ @@ -24,5 +24,6 @@ body { border: 1px solid #e2e8f0; border-radius: 8px; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); - margin: 0 auto; + margin: 0; + text-align: left; } From db9d46f752cff8332ad18edf5bcca04eb507bcc2 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 17:34:27 -0700 Subject: [PATCH 015/109] Improve docs. --- browser-extension/README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/browser-extension/README.md b/browser-extension/README.md index 543713b..9324f41 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -42,15 +42,15 @@ This is a [WXT](https://wxt.dev/)-based browser extension that ### Entry points -- `src/entrypoints/content.ts` - injected into every webpage -- `src/entrypoints/background.ts` - service worker that manages state and handles messages -- `src/entrypoints/popup` - React-based popup (html/css/tsx) with shadcn/ui table components +- [`src/entrypoints/content.ts`](src/entrypoints/content.ts) - injected into every webpage +- [`src/entrypoints/background.ts`](src/entrypoints/background.ts) - service worker that manages state and handles messages +- [`src/entrypoints/popup/popup.tsx`](src/entrypoints/popup/popup.tsx) - popup (html/css/tsx) with shadcn/ui table components ```mermaid graph TD Content[Content Script
content.ts] Background[Background Script
background.ts] - Popup[Popup Script
popup/main.tsx] + Popup[Popup Script
popup/popup.tsx] Content -->|ENHANCED/DESTROYED
CommentEvent| Background Popup -->|GET_OPEN_SPOTS
SWITCH_TO_TAB| Background @@ -69,8 +69,6 @@ graph TD class TextArea,UI ui ``` -### Architecture - Every time a `textarea` shows up on a page, on initial load or later on, it gets passed to a list of `CommentEnhancer`s. Each one gets a turn to say "I can enhance this box!". They show that they can enhance it by returning something non-null in the method `tryToEnhance(textarea: HTMLTextAreaElement): Spot | null`. Later on, that same `Spot` data will be used by the `tableRow(spot: Spot): ReactNode` method to create React components for rich formatting in the popup table. Those `Spot` values get bundled up with the `HTMLTextAreaElement` itself into an `EnhancedTextarea`, which gets added to the `TextareaRegistry`. At some interval, draft edits get saved by the browser extension. @@ -79,12 +77,12 @@ When the `textarea` gets removed from the page, the `TextareaRegistry` is notifi ## Testing -In `tests/har` there are various `.har` files. These are complete recordings of a single page load. - -- `pnpm run har:view` and you can see the recordings, with or without our browser extension. +- `npm run playground` gives you a test environment where you can tinker with the popup with various test data, supports hot reload +- `npm run har:view` gives you recordings of various web pages which you can see with and without enhancement by the browser extension ### Recording new HAR files +- the har recordings live in `tests/har`, they are complete recordings of the network requests of a single page load - you can add or change URLs in `tests/har-index.ts` - `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens - login manually, then close the browser From adadfd83d7ca013abcdb8c003d4b7c7566ef199a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 17:35:52 -0700 Subject: [PATCH 016/109] Change width to 311px. --- browser-extension/src/styles/popup-frame.css | 2 +- browser-extension/tests/playground/playground.tsx | 4 ++-- browser-extension/tests/playground/style.css | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/browser-extension/src/styles/popup-frame.css b/browser-extension/src/styles/popup-frame.css index b03d1a4..971d5fa 100644 --- a/browser-extension/src/styles/popup-frame.css +++ b/browser-extension/src/styles/popup-frame.css @@ -1,6 +1,6 @@ /* Popup window frame styles */ body { - width: 300px; + width: 311px; padding: 15px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index cac9281..a6f84ae 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -10,7 +10,7 @@ root.render(

Popup Simulator

- This shows exactly how the table appears in the browser popup (300px width). + This shows exactly how the table appears in the browser popup (311px width).

@@ -22,7 +22,7 @@ root.render(

Development Notes

  • - The popup frame above matches the exact 300px width of the browser extension popup + The popup frame above matches the exact 311px width of the browser extension popup
  • Any changes to popup/style.css will automatically update here diff --git a/browser-extension/tests/playground/style.css b/browser-extension/tests/playground/style.css index b1ada89..3538dae 100644 --- a/browser-extension/tests/playground/style.css +++ b/browser-extension/tests/playground/style.css @@ -16,7 +16,7 @@ body { /* Popup simulator frame */ .popup-frame { - width: 300px; + width: 311px; padding: 15px; font-size: 14px; line-height: 1.4; From 2ca0e680407298880703d1b9108aa0c080126ccc Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 17:40:28 -0700 Subject: [PATCH 017/109] Set the width in only one place. --- browser-extension/src/styles/popup-frame.css | 6 +++++- browser-extension/tests/playground/playground.tsx | 10 +--------- browser-extension/tests/playground/style.css | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/browser-extension/src/styles/popup-frame.css b/browser-extension/src/styles/popup-frame.css index 971d5fa..fd54bc9 100644 --- a/browser-extension/src/styles/popup-frame.css +++ b/browser-extension/src/styles/popup-frame.css @@ -1,6 +1,10 @@ /* Popup window frame styles */ +:root { + --popup-width: 311px; +} + body { - width: 311px; + width: var(--popup-width); padding: 15px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index a6f84ae..2ee3653 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -9,9 +9,6 @@ root.render(

    Popup Simulator

    -

    - This shows exactly how the table appears in the browser popup (311px width). -

    @@ -21,12 +18,7 @@ root.render(

    Development Notes

      -
    • - The popup frame above matches the exact 311px width of the browser extension popup -
    • -
    • - Any changes to popup/style.css will automatically update here -
    • +
    • The popup frame above matches the exact browser extension popup.
    • Hot reload is active for instant updates
    diff --git a/browser-extension/tests/playground/style.css b/browser-extension/tests/playground/style.css index 3538dae..c56cada 100644 --- a/browser-extension/tests/playground/style.css +++ b/browser-extension/tests/playground/style.css @@ -16,7 +16,7 @@ body { /* Popup simulator frame */ .popup-frame { - width: 311px; + width: var(--popup-width); padding: 15px; font-size: 14px; line-height: 1.4; From 0e17d9d641eed0f260f5035d170fde948ec6fb82 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 17:41:36 -0700 Subject: [PATCH 018/109] Make it wider. --- browser-extension/src/styles/popup-frame.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-extension/src/styles/popup-frame.css b/browser-extension/src/styles/popup-frame.css index fd54bc9..49cd2b8 100644 --- a/browser-extension/src/styles/popup-frame.css +++ b/browser-extension/src/styles/popup-frame.css @@ -1,6 +1,6 @@ /* Popup window frame styles */ :root { - --popup-width: 311px; + --popup-width: 600px; } body { From c04643c24404dfa7b404143bf3db0eb272717998 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 20:06:56 -0700 Subject: [PATCH 019/109] We were missing vite, which we were using through an undeclared transitive on wxt i think. --- browser-extension/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/browser-extension/package.json b/browser-extension/package.json index 00bb9a5..5516936 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -33,6 +33,7 @@ "tailwindcss": "^4.1.13", "tsx": "^4.19.1", "typescript": "^5.8.3", + "vite": "^7.1.5", "vitest": "^3.2.4", "wxt": "^0.20.7" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50f0e46..ea4a3ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.9.2 + vite: + specifier: ^7.1.5 + version: 7.1.5(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@22.18.1)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.5) From 10805c1c6783e57c26cebaa3141613afbe65fc48 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 22:07:16 -0700 Subject: [PATCH 020/109] Unnecessary docs. --- browser-extension/README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/browser-extension/README.md b/browser-extension/README.md index 9324f41..79275aa 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -31,15 +31,6 @@ This is a [WXT](https://wxt.dev/)-based browser extension that - finds `textarea` components and decorates them with [overtype](https://overtype.dev/) and [highlight.js](https://highlightjs.org/) - stores unposted comment drafts, and makes them easy to find via the extension popup -### Tech Stack - -- **Framework**: [WXT](https://wxt.dev/) for browser extension development -- **UI**: React with TypeScript JSX -- **Styling**: Tailwind CSS v4 (with first-party Vite plugin) -- **Components**: shadcn/ui for table components -- **Editor Enhancement**: [Overtype](https://overtype.dev/) with syntax highlighting -- **Build**: Vite with React plugin - ### Entry points - [`src/entrypoints/content.ts`](src/entrypoints/content.ts) - injected into every webpage From 0381d9b737f761900f8e2b4b1d19d67e967f2e72 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 22:08:25 -0700 Subject: [PATCH 021/109] Add the octicons. --- browser-extension/package.json | 1 + pnpm-lock.yaml | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/browser-extension/package.json b/browser-extension/package.json index 5516936..e2eabb0 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,6 +1,7 @@ { "author": "DiffPlug", "dependencies": { + "@primer/octicons-react": "^19.18.0", "@radix-ui/react-slot": "^1.2.3", "@types/react": "^19.1.12", "@types/react-dom": "^19.1.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea4a3ae..55dc316 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: browser-extension: dependencies: + '@primer/octicons-react': + specifier: ^19.18.0 + version: 19.18.0(react@19.1.1) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.12)(react@19.1.1) @@ -706,6 +709,12 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@primer/octicons-react@19.18.0': + resolution: {integrity: sha512-nLFlLmWfz3McbTiOUKVO+iwB15ALYQC9rHeP8K3qM1pyJ8svGaPjGR72BQSEM8ThyQUUodq/Re1n94tO5NNhzQ==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.3' + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -3923,6 +3932,10 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@primer/octicons-react@19.18.0(react@19.1.1)': + dependencies: + react: 19.1.1 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.12)(react@19.1.1)': dependencies: react: 19.1.1 From fbcfe43bde038aa00034572ef67f1035a6638343 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 23:09:08 -0700 Subject: [PATCH 022/109] Add some missing .gitignores --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 011e869..4ce6898 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ dist/ Thumbs.db # playright +.playwright-mcp/ +browser-extension/dist-playground/ browser-extension/playwright-report/ browser-extension/playwright/ -browser-extension/test-results/ \ No newline at end of file +browser-extension/test-results/ From 597cf79453304884b1a7691d8863aaab64886892 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 10 Sep 2025 23:28:56 -0700 Subject: [PATCH 023/109] Simplify. --- browser-extension/src/styles/globals.css | 70 +----------------------- 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/browser-extension/src/styles/globals.css b/browser-extension/src/styles/globals.css index 3094824..f1d8c73 100644 --- a/browser-extension/src/styles/globals.css +++ b/browser-extension/src/styles/globals.css @@ -1,69 +1 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - } - - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} +@import "tailwindcss"; From 72ce05d7d8e4c0bc8582e3acd619aec36fd83f80 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 08:18:04 -0700 Subject: [PATCH 024/109] Add a prototype straight outta claude. --- .../tests/playground/PopupPlayground.tsx | 8 +- browser-extension/tests/playground/claude.tsx | 582 ++++++++++++++++++ 2 files changed, 587 insertions(+), 3 deletions(-) create mode 100644 browser-extension/tests/playground/claude.tsx diff --git a/browser-extension/tests/playground/PopupPlayground.tsx b/browser-extension/tests/playground/PopupPlayground.tsx index 8a188f7..0833c34 100644 --- a/browser-extension/tests/playground/PopupPlayground.tsx +++ b/browser-extension/tests/playground/PopupPlayground.tsx @@ -1,4 +1,5 @@ import { SpotTable } from '@/components/SpotTable' +import DraftsTable from "./claude" import type { CommentState } from '@/entrypoints/background' import { EnhancerRegistry } from '@/lib/registries' import { sampleSpots } from './playgroundData' @@ -12,9 +13,10 @@ export function PopupPlayground() { return (
    -

    Open Comment Spots

    - + + {/*

    Open Comment Spots

    + DraftsTable -
    +
    */}
    ) } diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx new file mode 100644 index 0000000..085f424 --- /dev/null +++ b/browser-extension/tests/playground/claude.tsx @@ -0,0 +1,582 @@ +import { + AtSign, + CheckCircle2, + ChevronDown, + Circle, + Code, + ExternalLink, + GitCommit, + GitPullRequest, + Globe, + Link, + Lock, + MessageSquare, + Search, + Trash2, + XCircle, +} from 'lucide-react' +import React, { useMemo, useState } from 'react' + +// Mock data generator +const generateMockDrafts = () => [ + { + account: '@johnsmith', + charCount: 245, + content: + 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', + hasCode: true, + hasMention: true, + id: '1', + kind: 'PR', + lastEdit: Date.now() - 1000 * 60 * 30, + linkCount: 2, + number: 1234, + platform: 'GitHub', + private: true, + repoSlug: 'microsoft/vscode', + state: { type: 'open' }, + title: 'Fix memory leak in extension host', + url: 'https://github.com/microsoft/vscode/pull/1234', + }, + { + account: 'u/techwriter', + charCount: 180, + content: + "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", + hasCode: false, + hasMention: false, + id: '2', + kind: 'Comment', + lastEdit: Date.now() - 1000 * 60 * 60 * 2, + linkCount: 1, + platform: 'Reddit', + private: false, + repoSlug: 'r/programming', + state: { type: 'post' }, + title: "Re: What's your favorite VS Code extension?", + url: 'https://reddit.com/r/programming/comments/abc123', + }, + { + account: '@sarahdev', + charCount: 456, + content: + "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", + hasCode: true, + hasMention: false, + id: '3', + kind: 'Issue', + lastEdit: Date.now() - 1000 * 60 * 60 * 5, + linkCount: 0, + number: 5678, + platform: 'GitHub', + private: false, + repoSlug: 'facebook/react', + state: { type: 'open' }, + title: 'Unexpected behavior with useEffect cleanup', + url: 'https://github.com/facebook/react/issues/5678', + }, + { + account: '@alexcoder', + charCount: 322, + content: + 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', + hasCode: false, + hasMention: true, + id: '4', + kind: 'Review', + lastEdit: Date.now() - 1000 * 60 * 60 * 24, + linkCount: 3, + number: 9012, + platform: 'GitHub', + private: true, + repoSlug: 'vercel/next.js', + state: { type: 'merged' }, + title: 'Update routing documentation', + url: 'https://github.com/vercel/next.js/pull/9012', + }, + { + account: '@mikeeng', + charCount: 678, + content: + 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', + hasCode: true, + hasMention: true, + id: '5', + kind: 'PR', + lastEdit: Date.now() - 1000 * 60 * 60 * 48, + linkCount: 5, + number: 3456, + platform: 'GitHub', + private: false, + repoSlug: 'nodejs/node', + state: { type: 'closed' }, + title: 'Add support for ESM in worker threads', + url: 'https://github.com/nodejs/node/pull/3456', + }, +] + +// Helper function for relative time +const timeAgo = (date) => { + const seconds = Math.floor((Date.now() - date.getTime()) / 1000) + const intervals = [ + { label: 'y', secs: 31536000 }, + { label: 'mo', secs: 2592000 }, + { label: 'w', secs: 604800 }, + { label: 'd', secs: 86400 }, + { label: 'h', secs: 3600 }, + { label: 'm', secs: 60 }, + { label: 's', secs: 1 }, + ] + for (const i of intervals) { + const v = Math.floor(seconds / i.secs) + if (v >= 1) return `${v}${i.label} ago` + } + return 'just now' +} + +const DraftsTable = () => { + const [drafts] = useState(generateMockDrafts()) + const [selectedIds, setSelectedIds] = useState(new Set()) + const [platformFilter, setPlatformFilter] = useState('All') + const [typeFilter, setTypeFilter] = useState('All') + const [hasCodeFilter, setHasCodeFilter] = useState(false) + const [privateOnlyFilter, setPrivateOnlyFilter] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [sortBy, setSortBy] = useState('edited-newest') + + // Filter and sort drafts + const filteredDrafts = useMemo(() => { + let filtered = [...drafts] + + // Platform filter + if (platformFilter !== 'All') { + filtered = filtered.filter((d) => d.platform === platformFilter) + } + + // Type filter + if (typeFilter !== 'All') { + filtered = filtered.filter((d) => d.kind === typeFilter) + } + + // Has code filter + if (hasCodeFilter) { + filtered = filtered.filter((d) => d.hasCode) + } + + // Private only filter + if (privateOnlyFilter) { + filtered = filtered.filter((d) => d.private) + } + + // Search + if (searchQuery) { + const query = searchQuery.toLowerCase() + filtered = filtered.filter( + (d) => + d.title.toLowerCase().includes(query) || + d.content.toLowerCase().includes(query) || + d.repoSlug.toLowerCase().includes(query) || + (d.number && d.number.toString().includes(query)), + ) + } + + // Sort + switch (sortBy) { + case 'edited-newest': + filtered.sort((a, b) => b.lastEdit - a.lastEdit) + break + case 'edited-oldest': + filtered.sort((a, b) => a.lastEdit - b.lastEdit) + break + case 'title-asc': + filtered.sort((a, b) => a.title.localeCompare(b.title)) + break + } + + return filtered + }, [drafts, platformFilter, typeFilter, hasCodeFilter, privateOnlyFilter, searchQuery, sortBy]) + + const toggleSelection = (id) => { + const newSelected = new Set(selectedIds) + if (newSelected.has(id)) { + newSelected.delete(id) + } else { + newSelected.add(id) + } + setSelectedIds(newSelected) + } + + const toggleSelectAll = () => { + if (selectedIds.size === filteredDrafts.length && filteredDrafts.length > 0) { + setSelectedIds(new Set()) + } else { + setSelectedIds(new Set(filteredDrafts.map((d) => d.id))) + } + } + + const getStateIcon = (state) => { + switch (state.type) { + case 'open': + return + case 'merged': + return + case 'closed': + return + case 'post': + return + default: + return null + } + } + + const getKindIcon = (kind) => { + switch (kind) { + case 'PR': + return + case 'Issue': + return + case 'Review': + return + case 'Comment': + return + default: + return null + } + } + + const handleOpen = (url) => { + window.open(url, '_blank') + } + + const handleTrash = (draft) => { + if (draft.charCount > 20) { + if (confirm('Are you sure you want to discard this draft?')) { + console.log('Trashing draft:', draft.id) + } + } else { + console.log('Trashing draft:', draft.id) + } + } + + // Empty states + if (drafts.length === 0) { + return ( +
    +
    +

    No comments open

    +

    + Your drafts will appear here when you start typing in comment boxes across GitHub and + Reddit. +

    +
    + + · + +
    +
    +
    + ) + } + + if ( + filteredDrafts.length === 0 && + (searchQuery || + platformFilter !== 'All' || + typeFilter !== 'All' || + hasCodeFilter || + privateOnlyFilter) + ) { + return ( +
    +
    + {/* Keep the header controls visible */} +
    + {/* Platform filter */} + + + {/* Type filter */} + + + {/* Search */} +
    + + setSearchQuery(e.target.value)} + className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm' + /> +
    +
    +
    + +
    +

    No matches found

    + +
    +
    + ) + } + + return ( +
    + {/* Header controls */} +
    +
    + {/* Platform filter */} + + + {/* Type filter */} + + + {/* Toggle filters */} + + + + + {/* Sort */} + + + {/* Search */} +
    + + setSearchQuery(e.target.value)} + className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' + /> +
    +
    + + {/* Bulk actions bar */} + {selectedIds.size > 0 && ( +
    + {selectedIds.size} selected + + + + +
    + )} +
    + + {/* Table */} +
    + + + + + + + + + + + + + + + + + {filteredDrafts.map((draft) => ( + + + + + + + ))} + +
    + 0} + onChange={toggleSelectAll} + aria-label='Select all' + className='rounded' + /> + + Draft + + Edited + + Actions +
    + toggleSelection(draft.id)} + className='rounded' + /> + +
    + {/* Context line */} +
    + + {draft.platform === 'GitHub' ? '🐙' : '🔗'} + + + {draft.repoSlug} + + + {draft.private ? ( + + ) : ( + + )} + {draft.private ? 'Private' : 'Public'} + + + {getKindIcon(draft.kind)} + {draft.kind} + + {getStateIcon(draft.state)} + {draft.account} +
    + + {/* Title + snippet */} +
    + {draft.title} + — {draft.content.substring(0, 60)}… +
    + + {/* Signals row (hidden on small screens) */} +
    + {draft.hasCode && ( + + + code + + )} + {draft.hasMention && ( + + + mention + + )} + {draft.linkCount > 0 && ( + + + {draft.linkCount} + + )} + {draft.charCount} chars +
    +
    +
    + + {timeAgo(new Date(draft.lastEdit))} + + +
    + + +
    +
    +
    +
    + ) +} + +export default DraftsTable From 398548dc45e9c19c35c618f715070b5b70898653 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 08:26:03 -0700 Subject: [PATCH 025/109] Fake fix by manually creating the tailwind classes. --- .../tests/playground/playground.tsx | 1 + .../tests/playground/tailwind-fix.css | 162 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 browser-extension/tests/playground/tailwind-fix.css diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 2ee3653..4d96df8 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -1,6 +1,7 @@ import { createRoot } from 'react-dom/client' import '@/entrypoints/popup/style.css' import './style.css' +import './tailwind-fix.css' import { PopupPlayground } from './PopupPlayground' const root = createRoot(document.getElementById('root')!) diff --git a/browser-extension/tests/playground/tailwind-fix.css b/browser-extension/tests/playground/tailwind-fix.css new file mode 100644 index 0000000..3f7625b --- /dev/null +++ b/browser-extension/tests/playground/tailwind-fix.css @@ -0,0 +1,162 @@ +/* Tailwind utility classes for the component */ + +/* Background colors */ +.bg-white { background-color: #ffffff; } +.bg-gray-50 { background-color: #f9fafb; } +.bg-gray-100 { background-color: #f3f4f6; } +.bg-blue-50 { background-color: #eff6ff; } +.bg-blue-100 { background-color: #dbeafe; } +.bg-green-50 { background-color: #f0fdf4; } +.bg-purple-50 { background-color: #faf5ff; } +.bg-slate-50 { background-color: #f8fafc; } +.bg-slate-100 { background-color: #f1f5f9; } + +/* Text colors */ +.text-gray-500 { color: #6b7280; } +.text-gray-600 { color: #4b5563; } +.text-gray-900 { color: #111827; } +.text-slate-500 { color: #64748b; } +.text-slate-600 { color: #475569; } +.text-slate-900 { color: #0f172a; } +.text-blue-600 { color: #2563eb; } +.text-blue-700 { color: #1d4ed8; } +.text-green-700 { color: #15803d; } +.text-purple-700 { color: #7c3aed; } +.text-sky-500 { color: #0ea5e9; } +.text-emerald-500 { color: #10b981; } +.text-rose-500 { color: #f43f5e; } +.text-xs { font-size: 0.75rem; line-height: 1rem; } +.text-sm { font-size: 0.875rem; line-height: 1.25rem; } + +/* Spacing */ +.px-1\.5 { padding-left: 0.375rem; padding-right: 0.375rem; } +.py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; } +.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; } +.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; } +.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } +.p-1\.5 { padding: 0.375rem; } +.p-3 { padding: 0.75rem; } +.p-4 { padding: 1rem; } +.p-6 { padding: 1.5rem; } +.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } +.py-8 { padding-top: 2rem; padding-bottom: 2rem; } +.pl-9 { padding-left: 2.25rem; } +.pr-3 { padding-right: 0.75rem; } +.mt-3 { margin-top: 0.75rem; } +.mb-2 { margin-top: 0.5rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-6 { margin-bottom: 1.5rem; } +.mt-6 { margin-top: 1.5rem; } +.mx-auto { margin-left: auto; margin-right: auto; } + +/* Layout */ +.min-h-screen { min-height: 100vh; } +.w-full { width: 100%; } +.w-3 { width: 0.75rem; } +.w-4 { width: 1rem; } +.h-3 { height: 0.75rem; } +.h-4 { height: 1rem; } +.w-10 { width: 2.5rem; } +.w-24 { width: 6rem; } +.max-w-xs { max-width: 20rem; } +.max-w-2xl { max-width: 42rem; } +.max-w-4xl { max-width: 56rem; } + +/* Flexbox */ +.flex { display: flex; } +.inline-flex { display: inline-flex; } +.flex-1 { flex: 1 1 0%; } +.flex-wrap { flex-wrap: wrap; } +.items-center { align-items: center; } +.justify-center { align-items: center; } +.justify-end { justify-content: flex-end; } +.gap-0\.5 { gap: 0.125rem; } +.gap-1 { gap: 0.25rem; } +.gap-1\.5 { gap: 0.375rem; } +.gap-2 { gap: 0.5rem; } +.gap-3 { gap: 0.75rem; } +.space-y-1 > * + * { margin-top: 0.25rem; } +.space-y-2 > * + * { margin-top: 0.5rem; } + +/* Borders */ +.border { border-width: 1px; border-style: solid; border-color: #d1d5db; } +.border-b { border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #d1d5db; } +.border-gray-200 { border-color: #e5e7eb; } +.border-gray-300 { border-color: #d1d5db; } +.border-slate-200 { border-color: #e2e8f0; } +.divide-y > * + * { border-top-width: 1px; border-top-style: solid; border-top-color: #e5e7eb; } +.divide-gray-200 > * + * { border-top-color: #e5e7eb; } + +/* Border radius */ +.rounded { border-radius: 0.25rem; } +.rounded-md { border-radius: 0.375rem; } +.rounded-lg { border-radius: 0.5rem; } +.rounded-full { border-radius: 9999px; } + +/* Shadows */ +.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); } + +/* Position */ +.relative { position: relative; } +.absolute { position: absolute; } +.left-3 { left: 0.75rem; } +.top-1\/2 { top: 50%; } +.-translate-y-1\/2 { transform: translateY(-50%); } + +/* Typography */ +.font-medium { font-weight: 500; } +.font-semibold { font-weight: 600; } +.font-bold { font-weight: 700; } +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-2xl { font-size: 1.5rem; line-height: 2rem; } +.text-lg { font-size: 1.125rem; line-height: 1.75rem; } +.uppercase { text-transform: uppercase; } +.tracking-wider { letter-spacing: 0.05em; } +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Interactions */ +.cursor-pointer { cursor: pointer; } +.hover\:bg-gray-50:hover { background-color: #f9fafb; } +.hover\:bg-gray-100:hover { background-color: #f3f4f6; } +.hover\:underline:hover { text-decoration: underline; } +.transition-colors { + transition-property: color, background-color, border-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +/* Focus */ +.focus\:outline-none:focus { outline: none; } +.focus\:ring-2:focus { + box-shadow: 0 0 0 2px rgb(59 130 246 / 0.5); +} +.focus\:ring-blue-500:focus { + box-shadow: 0 0 0 2px rgb(59 130 246 / 0.5); +} + +/* Display */ +.hidden { display: none; } +.table-fixed { table-layout: fixed; } + +/* Overflow */ +.overflow-x-auto { overflow-x: auto; } + +/* Responsive */ +@media (min-width: 640px) { + .sm\:flex { display: flex; } +} + +/* Container */ +.container { + width: 100%; + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; +} \ No newline at end of file From 9c94e23281851a54b95f0a8ad926c3a922c599b8 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 08:32:38 -0700 Subject: [PATCH 026/109] Fix tailwind v4 --- .../src/entrypoints/popup/style.css | 5 +- .../tests/playground/playground.tsx | 1 - .../tests/playground/tailwind-fix.css | 162 ------------------ browser-extension/vite.playground.config.ts | 2 +- 4 files changed, 2 insertions(+), 168 deletions(-) delete mode 100644 browser-extension/tests/playground/tailwind-fix.css diff --git a/browser-extension/src/entrypoints/popup/style.css b/browser-extension/src/entrypoints/popup/style.css index 8e9d480..9cbe0f6 100644 --- a/browser-extension/src/entrypoints/popup/style.css +++ b/browser-extension/src/entrypoints/popup/style.css @@ -1,5 +1,2 @@ @import url("../../styles/popup-frame.css"); - -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 4d96df8..2ee3653 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -1,7 +1,6 @@ import { createRoot } from 'react-dom/client' import '@/entrypoints/popup/style.css' import './style.css' -import './tailwind-fix.css' import { PopupPlayground } from './PopupPlayground' const root = createRoot(document.getElementById('root')!) diff --git a/browser-extension/tests/playground/tailwind-fix.css b/browser-extension/tests/playground/tailwind-fix.css deleted file mode 100644 index 3f7625b..0000000 --- a/browser-extension/tests/playground/tailwind-fix.css +++ /dev/null @@ -1,162 +0,0 @@ -/* Tailwind utility classes for the component */ - -/* Background colors */ -.bg-white { background-color: #ffffff; } -.bg-gray-50 { background-color: #f9fafb; } -.bg-gray-100 { background-color: #f3f4f6; } -.bg-blue-50 { background-color: #eff6ff; } -.bg-blue-100 { background-color: #dbeafe; } -.bg-green-50 { background-color: #f0fdf4; } -.bg-purple-50 { background-color: #faf5ff; } -.bg-slate-50 { background-color: #f8fafc; } -.bg-slate-100 { background-color: #f1f5f9; } - -/* Text colors */ -.text-gray-500 { color: #6b7280; } -.text-gray-600 { color: #4b5563; } -.text-gray-900 { color: #111827; } -.text-slate-500 { color: #64748b; } -.text-slate-600 { color: #475569; } -.text-slate-900 { color: #0f172a; } -.text-blue-600 { color: #2563eb; } -.text-blue-700 { color: #1d4ed8; } -.text-green-700 { color: #15803d; } -.text-purple-700 { color: #7c3aed; } -.text-sky-500 { color: #0ea5e9; } -.text-emerald-500 { color: #10b981; } -.text-rose-500 { color: #f43f5e; } -.text-xs { font-size: 0.75rem; line-height: 1rem; } -.text-sm { font-size: 0.875rem; line-height: 1.25rem; } - -/* Spacing */ -.px-1\.5 { padding-left: 0.375rem; padding-right: 0.375rem; } -.py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; } -.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; } -.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; } -.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } -.p-1\.5 { padding: 0.375rem; } -.p-3 { padding: 0.75rem; } -.p-4 { padding: 1rem; } -.p-6 { padding: 1.5rem; } -.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } -.py-8 { padding-top: 2rem; padding-bottom: 2rem; } -.pl-9 { padding-left: 2.25rem; } -.pr-3 { padding-right: 0.75rem; } -.mt-3 { margin-top: 0.75rem; } -.mb-2 { margin-top: 0.5rem; } -.mb-4 { margin-bottom: 1rem; } -.mb-6 { margin-bottom: 1.5rem; } -.mt-6 { margin-top: 1.5rem; } -.mx-auto { margin-left: auto; margin-right: auto; } - -/* Layout */ -.min-h-screen { min-height: 100vh; } -.w-full { width: 100%; } -.w-3 { width: 0.75rem; } -.w-4 { width: 1rem; } -.h-3 { height: 0.75rem; } -.h-4 { height: 1rem; } -.w-10 { width: 2.5rem; } -.w-24 { width: 6rem; } -.max-w-xs { max-width: 20rem; } -.max-w-2xl { max-width: 42rem; } -.max-w-4xl { max-width: 56rem; } - -/* Flexbox */ -.flex { display: flex; } -.inline-flex { display: inline-flex; } -.flex-1 { flex: 1 1 0%; } -.flex-wrap { flex-wrap: wrap; } -.items-center { align-items: center; } -.justify-center { align-items: center; } -.justify-end { justify-content: flex-end; } -.gap-0\.5 { gap: 0.125rem; } -.gap-1 { gap: 0.25rem; } -.gap-1\.5 { gap: 0.375rem; } -.gap-2 { gap: 0.5rem; } -.gap-3 { gap: 0.75rem; } -.space-y-1 > * + * { margin-top: 0.25rem; } -.space-y-2 > * + * { margin-top: 0.5rem; } - -/* Borders */ -.border { border-width: 1px; border-style: solid; border-color: #d1d5db; } -.border-b { border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #d1d5db; } -.border-gray-200 { border-color: #e5e7eb; } -.border-gray-300 { border-color: #d1d5db; } -.border-slate-200 { border-color: #e2e8f0; } -.divide-y > * + * { border-top-width: 1px; border-top-style: solid; border-top-color: #e5e7eb; } -.divide-gray-200 > * + * { border-top-color: #e5e7eb; } - -/* Border radius */ -.rounded { border-radius: 0.25rem; } -.rounded-md { border-radius: 0.375rem; } -.rounded-lg { border-radius: 0.5rem; } -.rounded-full { border-radius: 9999px; } - -/* Shadows */ -.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); } - -/* Position */ -.relative { position: relative; } -.absolute { position: absolute; } -.left-3 { left: 0.75rem; } -.top-1\/2 { top: 50%; } -.-translate-y-1\/2 { transform: translateY(-50%); } - -/* Typography */ -.font-medium { font-weight: 500; } -.font-semibold { font-weight: 600; } -.font-bold { font-weight: 700; } -.text-left { text-align: left; } -.text-right { text-align: right; } -.text-center { text-align: center; } -.text-2xl { font-size: 1.5rem; line-height: 2rem; } -.text-lg { font-size: 1.125rem; line-height: 1.75rem; } -.uppercase { text-transform: uppercase; } -.tracking-wider { letter-spacing: 0.05em; } -.truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* Interactions */ -.cursor-pointer { cursor: pointer; } -.hover\:bg-gray-50:hover { background-color: #f9fafb; } -.hover\:bg-gray-100:hover { background-color: #f3f4f6; } -.hover\:underline:hover { text-decoration: underline; } -.transition-colors { - transition-property: color, background-color, border-color; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -/* Focus */ -.focus\:outline-none:focus { outline: none; } -.focus\:ring-2:focus { - box-shadow: 0 0 0 2px rgb(59 130 246 / 0.5); -} -.focus\:ring-blue-500:focus { - box-shadow: 0 0 0 2px rgb(59 130 246 / 0.5); -} - -/* Display */ -.hidden { display: none; } -.table-fixed { table-layout: fixed; } - -/* Overflow */ -.overflow-x-auto { overflow-x: auto; } - -/* Responsive */ -@media (min-width: 640px) { - .sm\:flex { display: flex; } -} - -/* Container */ -.container { - width: 100%; - margin-left: auto; - margin-right: auto; - padding-left: 1rem; - padding-right: 1rem; -} \ No newline at end of file diff --git a/browser-extension/vite.playground.config.ts b/browser-extension/vite.playground.config.ts index 37888e5..f007109 100644 --- a/browser-extension/vite.playground.config.ts +++ b/browser-extension/vite.playground.config.ts @@ -6,7 +6,7 @@ import path from 'path' export default defineConfig({ plugins: [ react(), - tailwindcss() as any + tailwindcss() ], resolve: { alias: { From e3b92ca102a481fea7c129edc000428aeb6c6c7d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 09:00:53 -0700 Subject: [PATCH 027/109] biome fixup --- browser-extension/tests/playground/claude.tsx | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 085f424..73135c0 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,7 +1,6 @@ import { AtSign, CheckCircle2, - ChevronDown, Circle, Code, ExternalLink, @@ -15,7 +14,7 @@ import { Trash2, XCircle, } from 'lucide-react' -import React, { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' // Mock data generator const generateMockDrafts = () => [ @@ -116,8 +115,9 @@ const generateMockDrafts = () => [ ] // Helper function for relative time -const timeAgo = (date) => { - const seconds = Math.floor((Date.now() - date.getTime()) / 1000) +const timeAgo = (date: Date | number) => { + const timestamp = typeof date === 'number' ? date : date.getTime() + const seconds = Math.floor((Date.now() - timestamp) / 1000) const intervals = [ { label: 'y', secs: 31536000 }, { label: 'mo', secs: 2592000 }, @@ -176,7 +176,7 @@ const DraftsTable = () => { d.title.toLowerCase().includes(query) || d.content.toLowerCase().includes(query) || d.repoSlug.toLowerCase().includes(query) || - (d.number && d.number.toString().includes(query)), + d.number?.toString().includes(query), ) } @@ -196,7 +196,7 @@ const DraftsTable = () => { return filtered }, [drafts, platformFilter, typeFilter, hasCodeFilter, privateOnlyFilter, searchQuery, sortBy]) - const toggleSelection = (id) => { + const toggleSelection = (id: string) => { const newSelected = new Set(selectedIds) if (newSelected.has(id)) { newSelected.delete(id) @@ -214,7 +214,7 @@ const DraftsTable = () => { } } - const getStateIcon = (state) => { + const getStateIcon = (state: { type: string }) => { switch (state.type) { case 'open': return @@ -229,7 +229,7 @@ const DraftsTable = () => { } } - const getKindIcon = (kind) => { + const getKindIcon = (kind: string) => { switch (kind) { case 'PR': return @@ -244,11 +244,11 @@ const DraftsTable = () => { } } - const handleOpen = (url) => { + const handleOpen = (url: string) => { window.open(url, '_blank') } - const handleTrash = (draft) => { + const handleTrash = (draft: { charCount: number; id: string }) => { if (draft.charCount > 20) { if (confirm('Are you sure you want to discard this draft?')) { console.log('Trashing draft:', draft.id) @@ -269,9 +269,13 @@ const DraftsTable = () => { Reddit.

    - + · - +
@@ -332,6 +336,7 @@ const DraftsTable = () => {

No matches found

- - - + + + +
)}
@@ -553,6 +566,7 @@ const DraftsTable = () => {
) } - -export default DraftsTable diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 2ee3653..0579631 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -1,27 +1,54 @@ import { createRoot } from 'react-dom/client' +import { useState } from 'react' import '@/entrypoints/popup/style.css' import './style.css' -import { PopupPlayground } from './PopupPlayground' +import { Replica } from './replica' +import { ClaudePrototype } from "./claude" -const root = createRoot(document.getElementById('root')!) -root.render( -
-
-
-

Popup Simulator

-
+type Mode = 'Replica' | 'ClaudePrototype' -
- -
+const App = () => { + const [activeComponent, setActiveComponent] = useState('Replica') -
-

Development Notes

-
    -
  • The popup frame above matches the exact browser extension popup.
  • -
  • Hot reload is active for instant updates
  • -
+ return ( +
+
+
+

Popup Simulator

+
    +
  • The popup frame is meant to exactly match the browser extension popup.
  • +
  • Hot reload is active for instant updates
  • +
+
+ + +
+
+ +
+ {activeComponent === 'Replica' && } + {activeComponent === 'ClaudePrototype' && } +
-
, -) + ) +} + +const root = createRoot(document.getElementById('root')!) +root.render() diff --git a/browser-extension/tests/playground/playgroundData.tsx b/browser-extension/tests/playground/replica.tsx similarity index 50% rename from browser-extension/tests/playground/playgroundData.tsx rename to browser-extension/tests/playground/replica.tsx index 57fd727..ee02cf7 100644 --- a/browser-extension/tests/playground/playgroundData.tsx +++ b/browser-extension/tests/playground/replica.tsx @@ -1,4 +1,7 @@ +import { SpotTable } from '@/components/SpotTable' import type { CommentState } from '@/entrypoints/background' +import { EnhancerRegistry } from '@/lib/registries' + import type { CommentSpot } from '@/lib/enhancer' import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' @@ -19,8 +22,7 @@ const gh_issue: GitHubIssueAddCommentSpot = { } const spots: CommentSpot[] = [gh_pr, gh_issue] - -export const sampleSpots: CommentState[] = spots.map((spot) => { +const sampleSpots: CommentState[] = spots.map((spot) => { const state: CommentState = { drafts: [ [ @@ -38,3 +40,31 @@ export const sampleSpots: CommentState[] = spots.map((spot) => { } return state }) + + +export function Replica() { + const handleSpotClick = (spot: CommentState) => { + alert(`Clicked: ${spot.spot.type}\nTab: ${spot.tab.tabId}`) + } + + const enhancers = new EnhancerRegistry() + + return ( +
+

Open Comment Spots

+ +
+ +
+
+ ) +} From bc3f58398678de83c7579ed0c936cb43174705b9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 09:30:15 -0700 Subject: [PATCH 029/109] Playground structure improvements. --- .../tests/playground/playground.tsx | 48 ++++++++++--------- .../tests/playground/replica.tsx | 4 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 0579631..ec658cf 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -1,14 +1,19 @@ -import { createRoot } from 'react-dom/client' import { useState } from 'react' +import { createRoot } from 'react-dom/client' import '@/entrypoints/popup/style.css' import './style.css' +import { ClaudePrototype } from './claude' import { Replica } from './replica' -import { ClaudePrototype } from "./claude" -type Mode = 'Replica' | 'ClaudePrototype' +const MODES = { + claude: { component: ClaudePrototype, label: 'claude' }, + replica: { component: Replica, label: 'replica' }, +} as const + +type Mode = keyof typeof MODES const App = () => { - const [activeComponent, setActiveComponent] = useState('Replica') + const [activeComponent, setActiveComponent] = useState('replica') return (
@@ -20,30 +25,27 @@ const App = () => {
  • Hot reload is active for instant updates
  • - - + > + {config.label} + + ))}
    - {activeComponent === 'Replica' && } - {activeComponent === 'ClaudePrototype' && } + {(() => { + const Component = MODES[activeComponent].component + return + })()}
    diff --git a/browser-extension/tests/playground/replica.tsx b/browser-extension/tests/playground/replica.tsx index ee02cf7..4a90d74 100644 --- a/browser-extension/tests/playground/replica.tsx +++ b/browser-extension/tests/playground/replica.tsx @@ -1,10 +1,9 @@ import { SpotTable } from '@/components/SpotTable' import type { CommentState } from '@/entrypoints/background' -import { EnhancerRegistry } from '@/lib/registries' - import type { CommentSpot } from '@/lib/enhancer' import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' +import { EnhancerRegistry } from '@/lib/registries' const gh_pr: GitHubPRAddCommentSpot = { domain: 'github.com', @@ -41,7 +40,6 @@ const sampleSpots: CommentState[] = spots.map((spot) => { return state }) - export function Replica() { const handleSpotClick = (spot: CommentState) => { alert(`Clicked: ${spot.spot.type}\nTab: ${spot.tab.tabId}`) From 592341d5e7e17797ec904ad9da6a6b6b1333fa4e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 09:31:33 -0700 Subject: [PATCH 030/109] fixup. --- browser-extension/tests/playground/playground.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index ec658cf..41933ce 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -28,6 +28,7 @@ const App = () => { {Object.entries(MODES).map(([mode, config]) => (
    Date: Thu, 11 Sep 2025 10:03:45 -0700 Subject: [PATCH 035/109] Tightened up a bit. --- browser-extension/tests/playground/claude.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 5bde30d..dfe134e 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -305,9 +305,18 @@ export const ClaudePrototype = () => { return (
    {/* Header controls */} -
    +
    - {/* Toggle filters */} +
    + + setSearchQuery(e.target.value)} + className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' + /> +
    - - - - {/* Search */} -
    - - setSearchQuery(e.target.value)} - className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' - /> -
    {/* Bulk actions bar */} @@ -397,7 +392,7 @@ export const ClaudePrototype = () => { onClick={() => setSortBy(sortBy === 'edited-newest' ? 'edited-oldest' : 'edited-newest')} className='flex items-center gap-1 hover:text-gray-700' > - Edited + EDITED {sortBy === 'edited-newest' ? ( ) : ( From 3a8e4d402e93a0b8862c4375038bffa378c258ff Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 10:06:24 -0700 Subject: [PATCH 036/109] Move search into the table header. --- browser-extension/tests/playground/claude.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index dfe134e..c828dc6 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -307,16 +307,6 @@ export const ClaudePrototype = () => { {/* Header controls */}
    -
    - - setSearchQuery(e.target.value)} - className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' - /> -
    Date: Thu, 11 Sep 2025 10:08:18 -0700 Subject: [PATCH 037/109] More compression. --- browser-extension/tests/playground/claude.tsx | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index c828dc6..1af088b 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -6,6 +6,7 @@ import { Circle, Code, ExternalLink, + Filter, GitCommit, GitPullRequest, Globe, @@ -142,6 +143,7 @@ export const ClaudePrototype = () => { const [privateOnlyFilter, setPrivateOnlyFilter] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [sortBy, setSortBy] = useState('edited-newest') + const [showFilters, setShowFilters] = useState(false) const filteredDrafts = useMemo(() => { let filtered = [...drafts] @@ -307,24 +309,6 @@ export const ClaudePrototype = () => { {/* Header controls */}
    - -
    {/* Bulk actions bar */} @@ -372,14 +356,50 @@ export const ClaudePrototype = () => { className='px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider' >
    - - setSearchQuery(e.target.value)} - className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' - /> +
    +
    + + setSearchQuery(e.target.value)} + className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' + /> +
    + +
    + {showFilters && ( +
    +
    + + +
    +
    + )}
    Date: Thu, 11 Sep 2025 10:14:30 -0700 Subject: [PATCH 038/109] Progress. --- browser-extension/tests/playground/claude.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 1af088b..139a5d6 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -370,14 +370,14 @@ export const ClaudePrototype = () => { {showFilters && ( -
    +
    {/* Title + snippet */} From 0859ffdb408aa5e7e5c929f117df4dc2621b092f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 10:18:36 -0700 Subject: [PATCH 040/109] Remove the public/private filter --- browser-extension/tests/playground/claude.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 43743f0..39876a1 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -447,14 +447,6 @@ export const ClaudePrototype = () => { > {draft.repoSlug} - - {draft.private ? ( - - ) : ( - - )} - {draft.private ? 'Private' : 'Public'} - {getKindIcon(draft.kind)} {draft.kind} From 5cd282659f2341acdca971f06c39c3b9be8b8687 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 10:26:58 -0700 Subject: [PATCH 041/109] Remove the private/public pills. --- browser-extension/tests/playground/claude.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 39876a1..818c99a 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -9,14 +9,13 @@ import { Filter, GitCommit, GitPullRequest, - Globe, Link, - Lock, MessageSquare, Search, Trash2, XCircle, } from 'lucide-react' +import { IssueOpenedIcon, GitPullRequestIcon } from '@primer/octicons-react' import { useMemo, useState } from 'react' // Mock data generator @@ -438,14 +437,19 @@ export const ClaudePrototype = () => { {/* Context line */}
    - {draft.platform === 'GitHub' ? '🐙' : '🔗'} + {draft.platform === 'GitHub' ? ( + draft.kind === 'PR' ? ( + + ) : draft.kind === 'Issue' ? ( + + ) : '🐙' + ) : '🔗'} - {draft.repoSlug} + #{draft.number} {draft.repoSlug} {getKindIcon(draft.kind)} From 88493d9c7859bea964f3c35ea8269ec656a0393e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 10:39:15 -0700 Subject: [PATCH 042/109] Replace mention count with image count. --- browser-extension/tests/playground/claude.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 818c99a..16aab61 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,7 +1,7 @@ import { ArrowDown, ArrowUp, - AtSign, + Image, CheckCircle2, Circle, Code, @@ -25,7 +25,7 @@ const generateMockDrafts = () => [ content: 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', hasCode: true, - hasMention: true, + hasImage: true, id: '1', kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 30, @@ -43,7 +43,7 @@ const generateMockDrafts = () => [ content: "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", hasCode: false, - hasMention: false, + hasImage: false, id: '2', kind: 'Comment', lastEdit: Date.now() - 1000 * 60 * 60 * 2, @@ -59,7 +59,7 @@ const generateMockDrafts = () => [ content: "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", hasCode: true, - hasMention: false, + hasImage: false, id: '3', kind: 'Issue', lastEdit: Date.now() - 1000 * 60 * 60 * 5, @@ -77,7 +77,7 @@ const generateMockDrafts = () => [ content: 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', hasCode: false, - hasMention: true, + hasImage: true, id: '4', kind: 'Review', lastEdit: Date.now() - 1000 * 60 * 60 * 24, @@ -95,7 +95,7 @@ const generateMockDrafts = () => [ content: 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', hasCode: true, - hasMention: true, + hasImage: true, id: '5', kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 60 * 48, @@ -466,22 +466,22 @@ export const ClaudePrototype = () => { {/* Signals row (hidden on small screens) */}
    - {draft.hasCode && ( + {draft.linkCount > 0 && ( - - code + + {draft.linkCount} )} - {draft.hasMention && ( + {draft.hasImage && ( - - mention + + image )} - {draft.linkCount > 0 && ( - - - {draft.linkCount} + {draft.hasCode && ( + + + code )} {draft.charCount} chars From 015cc84cf144de96ca387de5594f8001258ee2e6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 10:47:19 -0700 Subject: [PATCH 043/109] Remove dangling icons --- browser-extension/tests/playground/claude.tsx | 43 +------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 16aab61..be6ba08 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -2,18 +2,12 @@ import { ArrowDown, ArrowUp, Image, - CheckCircle2, - Circle, Code, ExternalLink, Filter, - GitCommit, - GitPullRequest, Link, - MessageSquare, Search, Trash2, - XCircle, } from 'lucide-react' import { IssueOpenedIcon, GitPullRequestIcon } from '@primer/octicons-react' import { useMemo, useState } from 'react' @@ -79,7 +73,7 @@ const generateMockDrafts = () => [ hasCode: false, hasImage: true, id: '4', - kind: 'Review', + kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 60 * 24, linkCount: 3, number: 9012, @@ -187,36 +181,6 @@ export const ClaudePrototype = () => { } } - const getStateIcon = (state: { type: string }) => { - switch (state.type) { - case 'open': - return - case 'merged': - return - case 'closed': - return - case 'post': - return - default: - return null - } - } - - const getKindIcon = (kind: string) => { - switch (kind) { - case 'PR': - return - case 'Issue': - return - case 'Review': - return - case 'Comment': - return - default: - return null - } - } - const handleOpen = (url: string) => { window.open(url, '_blank') } @@ -451,11 +415,6 @@ export const ClaudePrototype = () => { > #{draft.number} {draft.repoSlug} - - {getKindIcon(draft.kind)} - {draft.kind} - - {getStateIcon(draft.state)}
    {/* Title + snippet */} From 9109951a392d4f20114d8f6d30b689d533bb24d4 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 11:08:38 -0700 Subject: [PATCH 044/109] Lots of improvements. --- browser-extension/tests/playground/claude.tsx | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index be6ba08..8fb30a0 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -18,8 +18,8 @@ const generateMockDrafts = () => [ charCount: 245, content: 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', - hasCode: true, - hasImage: true, + codeCount: 3, + imageCount: 2, id: '1', kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 30, @@ -36,8 +36,8 @@ const generateMockDrafts = () => [ charCount: 180, content: "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", - hasCode: false, - hasImage: false, + codeCount: 0, + imageCount: 0, id: '2', kind: 'Comment', lastEdit: Date.now() - 1000 * 60 * 60 * 2, @@ -52,8 +52,8 @@ const generateMockDrafts = () => [ charCount: 456, content: "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", - hasCode: true, - hasImage: false, + codeCount: 1, + imageCount: 0, id: '3', kind: 'Issue', lastEdit: Date.now() - 1000 * 60 * 60 * 5, @@ -70,8 +70,8 @@ const generateMockDrafts = () => [ charCount: 322, content: 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', - hasCode: false, - hasImage: true, + codeCount: 0, + imageCount: 4, id: '4', kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 60 * 24, @@ -88,8 +88,8 @@ const generateMockDrafts = () => [ charCount: 678, content: 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', - hasCode: true, - hasImage: true, + codeCount: 7, + imageCount: 1, id: '5', kind: 'PR', lastEdit: Date.now() - 1000 * 60 * 60 * 48, @@ -128,7 +128,8 @@ export const ClaudePrototype = () => { const [drafts] = useState(generateMockDrafts()) const [selectedIds, setSelectedIds] = useState(new Set()) const [hasCodeFilter, setHasCodeFilter] = useState(false) - const [privateOnlyFilter, setPrivateOnlyFilter] = useState(false) + const [hasImageFilter, setHasImageFilter] = useState(false) + const [hasLinkFilter, setHasLinkFilter] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [sortBy, setSortBy] = useState('edited-newest') const [showFilters, setShowFilters] = useState(false) @@ -136,10 +137,13 @@ export const ClaudePrototype = () => { const filteredDrafts = useMemo(() => { let filtered = [...drafts] if (hasCodeFilter) { - filtered = filtered.filter((d) => d.hasCode) + filtered = filtered.filter((d) => d.codeCount > 0) } - if (privateOnlyFilter) { - filtered = filtered.filter((d) => d.private) + if (hasImageFilter) { + filtered = filtered.filter((d) => d.imageCount > 0) + } + if (hasLinkFilter) { + filtered = filtered.filter((d) => d.linkCount > 0) } if (searchQuery) { const query = searchQuery.toLowerCase() @@ -161,7 +165,7 @@ export const ClaudePrototype = () => { break } return filtered - }, [drafts, hasCodeFilter, privateOnlyFilter, searchQuery, sortBy]) + }, [drafts, hasCodeFilter, hasImageFilter, hasLinkFilter, searchQuery, sortBy]) const toggleSelection = (id: string) => { const newSelected = new Set(selectedIds) @@ -223,7 +227,8 @@ export const ClaudePrototype = () => { filteredDrafts.length === 0 && (searchQuery || hasCodeFilter || - privateOnlyFilter) + hasImageFilter || + hasLinkFilter) ) { return (
    @@ -250,7 +255,8 @@ export const ClaudePrototype = () => { type='button' onClick={() => { setHasCodeFilter(false) - setPrivateOnlyFilter(false) + setHasImageFilter(false) + setHasLinkFilter(false) setSearchQuery('') }} className='text-blue-600 hover:underline' @@ -311,7 +317,7 @@ export const ClaudePrototype = () => {
    @@ -340,20 +346,38 @@ export const ClaudePrototype = () => { +
    @@ -362,7 +386,7 @@ export const ClaudePrototype = () => {
    - Actions + ACTIONS
    {/* Context line */} -
    - - {draft.platform === 'GitHub' ? ( - draft.kind === 'PR' ? ( - - ) : draft.kind === 'Issue' ? ( - - ) : '🐙' - ) : '🔗'} - - - {draft.repoSlug.startsWith('r/') ? draft.repoSlug : - `#${draft.number} ${draft.repoSlug}` - } - +
    +
    + + {draft.platform === 'GitHub' ? ( + draft.kind === 'PR' ? ( + + ) : draft.kind === 'Issue' ? ( + + ) : '🐙' + ) : '🔗'} + + + {draft.repoSlug.startsWith('r/') ? draft.repoSlug : + `#${draft.number} ${draft.repoSlug}` + } + +
    +
    + {draft.linkCount > 0 && ( + + + {draft.linkCount} + + )} + {draft.imageCount > 0 && ( + + + {draft.imageCount} + + )} + {draft.codeCount > 0 && ( + + + {draft.codeCount} + + )} + + + {draft.charCount} + +
    - {/* Title + snippet */} + {/* Title */}
    {draft.title}
    + {/* Draft */}
    {draft.content.substring(0, 60)}…
    - - {/* Signals row (hidden on small screens) */} -
    - - {draft.linkCount > 0 && ( - - - {draft.linkCount} - - )} - {draft.imageCount > 0 && ( - - - {draft.imageCount} - - )} - {draft.codeCount > 0 && ( - - - {draft.codeCount} - - )} - {draft.charCount} chars -
    From b830d50b1e18dbccd1714fd6e69dce7e7c9296ed Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 11:35:58 -0700 Subject: [PATCH 047/109] More compact. --- browser-extension/tests/playground/claude.tsx | 55 ++++++++----------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index e9d1d18..2b1fac5 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -303,7 +303,6 @@ export const ClaudePrototype = () => {
    - ACTIONS -
    - - {timeAgo(new Date(draft.lastEdit))} - - -
    - - +
    + + {timeAgo(new Date(draft.lastEdit))} + +
    + + +
    - + {timeAgo(new Date(draft.lastEdit))}
    From e7b70f19ef27668459e57655e7885712f1c736d3 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 14:19:59 -0700 Subject: [PATCH 049/109] Bulk actions bar now floats. --- browser-extension/tests/playground/claude.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 58ebccc..f2b42cb 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -272,9 +272,9 @@ export const ClaudePrototype = () => {
    - {/* Bulk actions bar */} + {/* Bulk actions bar - floating popup */} {selectedIds.size > 0 && ( -
    +
    {selectedIds.size} selected
    - toggleSelection(draft.id)} - className='rounded' - /> - -
    - {/* Context line */} -
    -
    - - {draft.type === 'PR' && } - {draft.type === 'ISSUE' && } - {draft.type === 'REDDIT' && ( - Reddit - )} - - {} - {isGitHubDraft(draft) && ( - <> - - #{draft.number} - {' '} - - {draft.repoSlug} - - - )} - {isRedditDraft(draft) && ( - - r/{draft.subreddit} - - )} -
    -
    - {draft.linkCount > 0 && ( - - - {draft.linkCount} - - )} - {draft.imageCount > 0 && ( - - - {draft.imageCount} - - )} - {draft.codeCount > 0 && ( - - - {draft.codeCount} - - )} - - - {draft.charCount} - -
    -
    - - {/* Title */} -
    - {draft.title} -
    - {/* Draft */} -
    - {draft.content.substring(0, 60)}… -
    -
    -
    -
    - - {timeAgo(new Date(draft.lastEdit))} - -
    - - -
    -
    -
    ) } +function commentRow( + draft: Draft, + selectedIds: Set, + toggleSelection: (id: string) => void, + handleOpen: (url: string) => void, + handleTrash: (draft: { charCount: number; id: string }) => void, +) { + return ( + + + toggleSelection(draft.id)} + className='rounded' + /> + + +
    + {/* Context line */} +
    +
    + + {draft.type === 'PR' && } + {draft.type === 'ISSUE' && } + {draft.type === 'REDDIT' && ( + Reddit + )} + + + {isGitHubDraft(draft) && ( + <> + + #{draft.number} + {' '} + + {draft.repoSlug} + + + )} + {isRedditDraft(draft) && ( + + r/{draft.subreddit} + + )} +
    +
    + {draft.linkCount > 0 && ( + + + {draft.linkCount} + + )} + {draft.imageCount > 0 && ( + + + {draft.imageCount} + + )} + {draft.codeCount > 0 && ( + + + {draft.codeCount} + + )} + + + {draft.charCount} + +
    +
    + + {/* Title */} +
    + {draft.title} +
    + {/* Draft */} +
    + {draft.content.substring(0, 60)}… +
    +
    + + +
    + + {timeAgo(new Date(draft.lastEdit))} + +
    + + +
    +
    + + + ) +} From 98d7c3c630b4aa908c9733eb6b55a7dfc987140e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 15:57:21 -0700 Subject: [PATCH 054/109] Minor improvement. --- browser-extension/tests/playground/claude.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 80aabe0..1a6e122 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -480,9 +480,7 @@ function commentRow( {isGitHubDraft(draft) && ( <> - - #{draft.number} - {' '} + #{draft.number} {draft.repoSlug} @@ -521,8 +519,10 @@ function commentRow( {/* Title */} -
    - {draft.title} + {/* Draft */}
    From 66f73b0f3058327307af7fda8140d63b476eacd1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 16:04:40 -0700 Subject: [PATCH 055/109] Add a last-edit crumb. --- browser-extension/tests/playground/claude.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 1a6e122..e0dc3c0 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -9,6 +9,7 @@ import { Image, Link, Search, + Clock, TextSelect, Trash2, } from 'lucide-react' @@ -157,7 +158,7 @@ const timeAgo = (date: Date | number) => { ] for (const i of intervals) { const v = Math.floor(seconds / i.secs) - if (v >= 1) return `${v}${i.label} ago` + if (v >= 1) return `${v}${i.label}` } return 'just now' } @@ -515,6 +516,10 @@ function commentRow( {draft.charCount} + + + {timeAgo(draft.lastEdit)} +
    From b9e79d3ae0456ba7572f49e9890a4f8f52376378 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 16:10:22 -0700 Subject: [PATCH 056/109] Cleaner. --- browser-extension/tests/playground/claude.tsx | 69 ++----------------- 1 file changed, 5 insertions(+), 64 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index e0dc3c0..bd78a64 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,18 +1,6 @@ //import { DraftStats } from '@/lib/enhancers/draftStats' import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' -import { - ArrowDown, - ArrowUp, - Code, - ExternalLink, - Filter, - Image, - Link, - Search, - Clock, - TextSelect, - Trash2, -} from 'lucide-react' +import { Clock, Code, Filter, Image, Link, Search, TextSelect } from 'lucide-react' import { useMemo, useState } from 'react' /* @@ -170,7 +158,7 @@ export const ClaudePrototype = () => { const [hasImageFilter, setHasImageFilter] = useState(false) const [hasLinkFilter, setHasLinkFilter] = useState(false) const [searchQuery, setSearchQuery] = useState('') - const [sortBy, setSortBy] = useState('edited-newest') + const [sortBy, _setSortBy] = useState('edited-newest') const [showFilters, setShowFilters] = useState(false) const filteredDrafts = useMemo(() => { @@ -332,7 +320,6 @@ export const ClaudePrototype = () => { - @@ -414,25 +401,6 @@ export const ClaudePrototype = () => { )} - - - @@ -449,8 +417,8 @@ function commentRow( draft: Draft, selectedIds: Set, toggleSelection: (id: string) => void, - handleOpen: (url: string) => void, - handleTrash: (draft: { charCount: number; id: string }) => void, + _handleOpen: (url: string) => void, + _handleTrash: (draft: { charCount: number; id: string }) => void, ) { return ( @@ -531,34 +499,7 @@ function commentRow( {/* Draft */}
    - {draft.content.substring(0, 60)}… -
    - - - -
    - - {timeAgo(new Date(draft.lastEdit))} - -
    - - + {draft.content.substring(0, 100)}…
    From 856f3c538042b322e24823dda0b5abdc7e38b2ec Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 17:00:21 -0700 Subject: [PATCH 057/109] starting point --- browser-extension/tests/playground/claude.tsx | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index bd78a64..a909ba3 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,4 +1,6 @@ //import { DraftStats } from '@/lib/enhancers/draftStats' +import { CommentSpot } from '@/lib/enhancer' +import { DraftStats } from '@/lib/enhancers/draftStats' import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' import { Clock, Code, Filter, Image, Link, Search, TextSelect } from 'lucide-react' import { useMemo, useState } from 'react' @@ -21,9 +23,10 @@ export interface GitHubPRAddCommentSpot extends CommentSpot { */ type DraftType = 'PR' | 'ISSUE' | 'REDDIT' +type DraftState = 'EDITING' | 'ABANDONED' | 'SENT' +type TabState = 'OPEN_NOW' | 'CLOSED' -interface BaseDraft { - id: string +interface BaseDraft extends CommentSpot { charCount: number codeCount: number content: string @@ -31,19 +34,26 @@ interface BaseDraft { type: DraftType lastEdit: number linkCount: number - title: string - url: string } interface GitHubDraft extends BaseDraft { - repoSlug: string + title: string + slug: string number: number } interface RedditDraft extends BaseDraft { + title: string subreddit: string } +interface LatestDraft { + spot: BaseDraft, + draft: string, + time: number + draftStats: DraftStats +} + type Draft = GitHubDraft | RedditDraft const isGitHubDraft = (draft: Draft): draft is GitHubDraft => { @@ -60,74 +70,69 @@ const generateMockDrafts = (): Draft[] => [ codeCount: 3, content: 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', - id: '1', + unique_key: '1', imageCount: 2, lastEdit: Date.now() - 1000 * 60 * 30, linkCount: 2, number: 1234, - repoSlug: 'microsoft/vscode', + slug: 'microsoft/vscode', title: 'Fix memory leak in extension host', type: 'PR', - url: 'https://github.com/microsoft/vscode/pull/1234', } satisfies GitHubDraft, { charCount: 180, codeCount: 0, content: "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", - id: '2', + unique_key: '2', imageCount: 0, lastEdit: Date.now() - 1000 * 60 * 60 * 2, linkCount: 1, subreddit: 'programming', title: "Re: What's your favorite VS Code extension?", type: 'REDDIT', - url: 'https://reddit.com/r/programming/comments/abc123', } satisfies RedditDraft, { charCount: 456, codeCount: 1, content: "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", - id: '3', + unique_key: '3', imageCount: 0, lastEdit: Date.now() - 1000 * 60 * 60 * 5, linkCount: 0, number: 5678, - repoSlug: 'facebook/react', + slug: 'facebook/react', title: 'Unexpected behavior with useEffect cleanup', type: 'ISSUE', - url: 'https://github.com/facebook/react/issues/5678', } satisfies GitHubDraft, { charCount: 322, codeCount: 0, content: 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', - id: '4', + unique_key: '4', imageCount: 4, lastEdit: Date.now() - 1000 * 60 * 60 * 24, linkCount: 3, number: 9012, - repoSlug: 'vercel/next.js', + slug: 'vercel/next.js', title: 'Update routing documentation', type: 'PR', - url: 'https://github.com/vercel/next.js/pull/9012', } satisfies GitHubDraft, { charCount: 678, codeCount: 7, content: 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', - id: '5', + unique_key: '5', imageCount: 1, lastEdit: Date.now() - 1000 * 60 * 60 * 48, linkCount: 5, number: 3456, - repoSlug: 'nodejs/node', + slug: 'nodejs/node', title: 'Add support for ESM in worker threads', type: 'PR', - url: 'https://github.com/nodejs/node/pull/3456', } satisfies GitHubDraft, ] @@ -204,7 +209,7 @@ export const ClaudePrototype = () => { if (selectedIds.size === filteredDrafts.length && filteredDrafts.length > 0) { setSelectedIds(new Set()) } else { - setSelectedIds(new Set(filteredDrafts.map((d) => d.id))) + setSelectedIds(new Set(filteredDrafts.map((d) => d.unique_key))) } } @@ -421,12 +426,12 @@ function commentRow( _handleTrash: (draft: { charCount: number; id: string }) => void, ) { return ( - + toggleSelection(draft.id)} + checked={selectedIds.has(draft.unique_key)} + onChange={() => toggleSelection(draft.unique_key)} className='rounded' /> @@ -451,7 +456,7 @@ function commentRow( <> #{draft.number} - {draft.repoSlug} + {draft.slug} )} From 874acc1f0e0238e6d8ad0abde2a46b9bf4c33be8 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 17:07:11 -0700 Subject: [PATCH 058/109] Use CVA to make our little badge things more maintainable. --- browser-extension/tests/playground/claude.tsx | 107 ++++++++++-------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index a909ba3..3762f73 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,9 +1,52 @@ //import { DraftStats } from '@/lib/enhancers/draftStats' -import { CommentSpot } from '@/lib/enhancer' -import { DraftStats } from '@/lib/enhancers/draftStats' + import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' +import { cva, type VariantProps } from 'class-variance-authority' import { Clock, Code, Filter, Image, Link, Search, TextSelect } from 'lucide-react' import { useMemo, useState } from 'react' +import type { CommentSpot } from '@/lib/enhancer' +import type { DraftStats } from '@/lib/enhancers/draftStats' + +// CVA configuration for stat badges +const statBadge = cva('inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs', { + defaultVariants: { + type: 'text', + }, + variants: { + type: { + code: 'bg-pink-50 text-pink-700', + image: 'bg-purple-50 text-purple-700', + link: 'bg-blue-50 text-blue-700', + text: 'bg-gray-50 text-gray-700', + time: 'bg-gray-50 text-gray-700', + }, + }, +}) + +// Map types to their icons +const typeIcons = { + code: Code, + image: Image, + link: Link, + text: TextSelect, + time: Clock, +} as const + +// StatBadge component +type BadgeProps = VariantProps & { + text: number | string + type: keyof typeof typeIcons +} + +const Badge = ({ text, type }: BadgeProps) => { + const Icon = typeIcons[type] + return ( + + + {text} + + ) +} /* interface GitHubIssueAddCommentSpot extends CommentSpot { @@ -48,8 +91,8 @@ interface RedditDraft extends BaseDraft { } interface LatestDraft { - spot: BaseDraft, - draft: string, + spot: BaseDraft + draft: string time: number draftStats: DraftStats } @@ -70,7 +113,6 @@ const generateMockDrafts = (): Draft[] => [ codeCount: 3, content: 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', - unique_key: '1', imageCount: 2, lastEdit: Date.now() - 1000 * 60 * 30, linkCount: 2, @@ -78,26 +120,26 @@ const generateMockDrafts = (): Draft[] => [ slug: 'microsoft/vscode', title: 'Fix memory leak in extension host', type: 'PR', + unique_key: '1', } satisfies GitHubDraft, { charCount: 180, codeCount: 0, content: "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", - unique_key: '2', imageCount: 0, lastEdit: Date.now() - 1000 * 60 * 60 * 2, linkCount: 1, subreddit: 'programming', title: "Re: What's your favorite VS Code extension?", type: 'REDDIT', + unique_key: '2', } satisfies RedditDraft, { charCount: 456, codeCount: 1, content: "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", - unique_key: '3', imageCount: 0, lastEdit: Date.now() - 1000 * 60 * 60 * 5, linkCount: 0, @@ -105,13 +147,13 @@ const generateMockDrafts = (): Draft[] => [ slug: 'facebook/react', title: 'Unexpected behavior with useEffect cleanup', type: 'ISSUE', + unique_key: '3', } satisfies GitHubDraft, { charCount: 322, codeCount: 0, content: 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', - unique_key: '4', imageCount: 4, lastEdit: Date.now() - 1000 * 60 * 60 * 24, linkCount: 3, @@ -119,13 +161,13 @@ const generateMockDrafts = (): Draft[] => [ slug: 'vercel/next.js', title: 'Update routing documentation', type: 'PR', + unique_key: '4', } satisfies GitHubDraft, { charCount: 678, codeCount: 7, content: 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', - unique_key: '5', imageCount: 1, lastEdit: Date.now() - 1000 * 60 * 60 * 48, linkCount: 5, @@ -133,6 +175,7 @@ const generateMockDrafts = (): Draft[] => [ slug: 'nodejs/node', title: 'Add support for ESM in worker threads', type: 'PR', + unique_key: '5', } satisfies GitHubDraft, ] @@ -372,10 +415,7 @@ export const ClaudePrototype = () => { onChange={(e) => setHasLinkFilter(e.target.checked)} className='rounded' /> - - - links - + @@ -467,32 +501,11 @@ function commentRow( )}
    - {draft.linkCount > 0 && ( - - - {draft.linkCount} - - )} - {draft.imageCount > 0 && ( - - - {draft.imageCount} - - )} - {draft.codeCount > 0 && ( - - - {draft.codeCount} - - )} - - - {draft.charCount} - - - - {timeAgo(draft.lastEdit)} - + {draft.linkCount > 0 && } + {draft.imageCount > 0 && } + {draft.codeCount > 0 && } + +
    From 4981ed17b5f2fcacd71222fd061bd3c4e617b222 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 19:01:09 -0700 Subject: [PATCH 059/109] Massage the types to better fit where we're headed. --- browser-extension/tests/playground/claude.tsx | 338 +++++++++++------- 1 file changed, 215 insertions(+), 123 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 3762f73..a6b32d0 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -65,118 +65,202 @@ export interface GitHubPRAddCommentSpot extends CommentSpot { } */ -type DraftType = 'PR' | 'ISSUE' | 'REDDIT' -type DraftState = 'EDITING' | 'ABANDONED' | 'SENT' -type TabState = 'OPEN_NOW' | 'CLOSED' - -interface BaseDraft extends CommentSpot { - charCount: number - codeCount: number - content: string - imageCount: number - type: DraftType - lastEdit: number - linkCount: number -} - -interface GitHubDraft extends BaseDraft { +interface GitHubSpot extends CommentSpot { title: string slug: string number: number + type: 'PR' | 'ISSUE' } -interface RedditDraft extends BaseDraft { +interface RedditSpot extends CommentSpot { title: string subreddit: string + type: 'REDDIT' } -interface LatestDraft { - spot: BaseDraft - draft: string +interface Draft { + content: string time: number - draftStats: DraftStats + stats: DraftStats } -type Draft = GitHubDraft | RedditDraft +interface CommentTableRow { + spot: GitHubOrReddit + latestDraft: Draft + isOpenTab: boolean + isSent: boolean + isArchived: boolean +} + +type GitHubOrReddit = GitHubSpot | RedditSpot -const isGitHubDraft = (draft: Draft): draft is GitHubDraft => { - return draft.type === 'PR' || draft.type === 'ISSUE' +const isGitHubDraft = (spot: GitHubOrReddit): spot is GitHubSpot => { + return spot.type === 'PR' || spot.type === 'ISSUE' } -const isRedditDraft = (draft: Draft): draft is RedditDraft => { - return draft.type === 'REDDIT' +const isRedditDraft = (spot: GitHubOrReddit): spot is RedditSpot => { + return spot.type === 'REDDIT' } -const generateMockDrafts = (): Draft[] => [ +const generateMockDrafts = (): CommentTableRow[] => [ { - charCount: 245, - codeCount: 3, - content: - 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', - imageCount: 2, - lastEdit: Date.now() - 1000 * 60 * 30, - linkCount: 2, - number: 1234, - slug: 'microsoft/vscode', - title: 'Fix memory leak in extension host', - type: 'PR', - unique_key: '1', - } satisfies GitHubDraft, + isArchived: false, + isOpenTab: true, + isSent: false, + latestDraft: { + content: + 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', + stats: { + charCount: 245, + codeBlocks: [ + { code: 'const listener = () => {}', language: 'typescript' }, + { code: 'element.removeEventListener()', language: 'javascript' }, + { code: 'dispose()', language: 'typescript' }, + ], + images: [ + { url: 'https://example.com/image1.png' }, + { url: 'https://example.com/image2.png' }, + ], + links: [ + { text: 'Issue #1233', url: 'https://github.com/microsoft/vscode/issues/1233' }, + { text: 'Documentation', url: 'https://docs.microsoft.com' }, + ], + }, + time: Date.now() - 1000 * 60 * 30, + }, + spot: { + number: 1234, + slug: 'microsoft/vscode', + title: 'Fix memory leak in extension host', + type: 'PR', + unique_key: '1', + } satisfies GitHubSpot, + }, { - charCount: 180, - codeCount: 0, - content: - "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", - imageCount: 0, - lastEdit: Date.now() - 1000 * 60 * 60 * 2, - linkCount: 1, - subreddit: 'programming', - title: "Re: What's your favorite VS Code extension?", - type: 'REDDIT', - unique_key: '2', - } satisfies RedditDraft, + isArchived: false, + isOpenTab: false, + isSent: false, + latestDraft: { + content: + "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", + stats: { + charCount: 180, + codeBlocks: [], + images: [], + links: [ + { + text: 'GitLens', + url: 'https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens', + }, + ], + }, + time: Date.now() - 1000 * 60 * 60 * 2, + }, + spot: { + subreddit: 'programming', + title: "Re: What's your favorite VS Code extension?", + type: 'REDDIT', + unique_key: '2', + } satisfies RedditSpot, + }, { - charCount: 456, - codeCount: 1, - content: - "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", - imageCount: 0, - lastEdit: Date.now() - 1000 * 60 * 60 * 5, - linkCount: 0, - number: 5678, - slug: 'facebook/react', - title: 'Unexpected behavior with useEffect cleanup', - type: 'ISSUE', - unique_key: '3', - } satisfies GitHubDraft, + isArchived: false, + isOpenTab: true, + isSent: false, + latestDraft: { + content: + "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", + stats: { + charCount: 456, + codeBlocks: [{ code: 'useEffect(() => { /* async code */ }, [])', language: 'javascript' }], + images: [], + links: [], + }, + time: Date.now() - 1000 * 60 * 60 * 5, + }, + spot: { + number: 5678, + slug: 'facebook/react', + title: 'Unexpected behavior with useEffect cleanup', + type: 'ISSUE', + unique_key: '3', + } satisfies GitHubSpot, + }, { - charCount: 322, - codeCount: 0, - content: - 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', - imageCount: 4, - lastEdit: Date.now() - 1000 * 60 * 60 * 24, - linkCount: 3, - number: 9012, - slug: 'vercel/next.js', - title: 'Update routing documentation', - type: 'PR', - unique_key: '4', - } satisfies GitHubDraft, + isArchived: false, + isOpenTab: false, + isSent: true, + latestDraft: { + content: + 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', + stats: { + charCount: 322, + codeBlocks: [], + images: [ + { url: 'routing-diagram.png' }, + { url: 'example-1.png' }, + { url: 'example-2.png' }, + { url: 'architecture.png' }, + ], + links: [ + { text: 'Routing docs', url: 'https://nextjs.org/docs/routing' }, + { text: 'Examples', url: 'https://github.com/vercel/next.js/tree/main/examples' }, + { + text: 'Migration guide', + url: 'https://nextjs.org/docs/app/building-your-application/upgrading', + }, + ], + }, + time: Date.now() - 1000 * 60 * 60 * 24, + }, + spot: { + number: 9012, + slug: 'vercel/next.js', + title: 'Update routing documentation', + type: 'PR', + unique_key: '4', + } satisfies GitHubSpot, + }, { - charCount: 678, - codeCount: 7, - content: - 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', - imageCount: 1, - lastEdit: Date.now() - 1000 * 60 * 60 * 48, - linkCount: 5, - number: 3456, - slug: 'nodejs/node', - title: 'Add support for ESM in worker threads', - type: 'PR', - unique_key: '5', - } satisfies GitHubDraft, + isArchived: true, + isOpenTab: true, + isSent: false, + latestDraft: { + content: + 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', + stats: { + charCount: 678, + codeBlocks: [ + { code: 'import { Worker } from "worker_threads"', language: 'javascript' }, + { code: 'new Worker("./worker.mjs", { type: "module" })', language: 'javascript' }, + { code: 'import { parentPort } from "worker_threads"', language: 'javascript' }, + { code: 'interface WorkerOptions { type: "module" }', language: 'typescript' }, + { code: 'await import("./dynamic-module.mjs")', language: 'javascript' }, + { code: 'export default function workerTask() {}', language: 'javascript' }, + { code: 'const result = await workerPromise', language: 'javascript' }, + ], + images: [{ alt: 'ESM Worker Architecture', url: 'worker-architecture.png' }], + links: [ + { + text: 'TSC Meeting Notes', + url: 'https://github.com/nodejs/TSC/blob/main/meetings/2023-11-01.md', + }, + { text: 'ESM Spec', url: 'https://tc39.es/ecma262/' }, + { text: 'Worker Threads docs', url: 'https://nodejs.org/api/worker_threads.html' }, + { text: 'Implementation guide', url: 'https://nodejs.org/api/esm.html' }, + { text: 'Related issue', url: 'https://github.com/nodejs/node/issues/30682' }, + ], + }, + time: Date.now() - 1000 * 60 * 60 * 48, + }, + spot: { + number: 3456, + slug: 'nodejs/node', + title: 'Add support for ESM in worker threads', + type: 'PR', + unique_key: '5', + } satisfies GitHubSpot, + }, ] // Helper function for relative time @@ -212,27 +296,29 @@ export const ClaudePrototype = () => { const filteredDrafts = useMemo(() => { let filtered = [...drafts] if (hasCodeFilter) { - filtered = filtered.filter((d) => d.codeCount > 0) + filtered = filtered.filter((d) => d.latestDraft.stats.codeBlocks.length > 0) } if (hasImageFilter) { - filtered = filtered.filter((d) => d.imageCount > 0) + filtered = filtered.filter((d) => d.latestDraft.stats.images.length > 0) } if (hasLinkFilter) { - filtered = filtered.filter((d) => d.linkCount > 0) + filtered = filtered.filter((d) => d.latestDraft.stats.links.length > 0) } if (searchQuery) { const query = searchQuery.toLowerCase() filtered = filtered.filter((d) => - Object.values(d).some((value) => String(value).toLowerCase().includes(query)), + [d.spot.title, d.latestDraft.content, (d.spot as any).slug, (d.spot as any).subreddit].some( + (value) => value && String(value).toLowerCase().includes(query), + ), ) } // Sort switch (sortBy) { case 'edited-newest': - filtered.sort((a, b) => b.lastEdit - a.lastEdit) + filtered.sort((a, b) => b.latestDraft.time - a.latestDraft.time) break case 'edited-oldest': - filtered.sort((a, b) => a.lastEdit - b.lastEdit) + filtered.sort((a, b) => a.latestDraft.time - b.latestDraft.time) break } return filtered @@ -252,7 +338,7 @@ export const ClaudePrototype = () => { if (selectedIds.size === filteredDrafts.length && filteredDrafts.length > 0) { setSelectedIds(new Set()) } else { - setSelectedIds(new Set(filteredDrafts.map((d) => d.unique_key))) + setSelectedIds(new Set(filteredDrafts.map((d) => d.spot.unique_key))) } } @@ -260,13 +346,13 @@ export const ClaudePrototype = () => { window.open(url, '_blank') } - const handleTrash = (draft: { charCount: number; id: string }) => { - if (draft.charCount > 20) { + const handleTrash = (row: CommentTableRow) => { + if (row.latestDraft.stats.charCount > 20) { if (confirm('Are you sure you want to discard this draft?')) { - console.log('Trashing draft:', draft.id) + console.log('Trashing draft:', row.spot.unique_key) } } else { - console.log('Trashing draft:', draft.id) + console.log('Trashing draft:', row.spot.unique_key) } } @@ -443,8 +529,8 @@ export const ClaudePrototype = () => { - {filteredDrafts.map((draft) => - commentRow(draft, selectedIds, toggleSelection, handleOpen, handleTrash), + {filteredDrafts.map((row) => + commentRow(row, selectedIds, toggleSelection, handleOpen, handleTrash), )} @@ -453,19 +539,19 @@ export const ClaudePrototype = () => { ) } function commentRow( - draft: Draft, + row: CommentTableRow, selectedIds: Set, toggleSelection: (id: string) => void, _handleOpen: (url: string) => void, - _handleTrash: (draft: { charCount: number; id: string }) => void, + _handleTrash: (row: CommentTableRow) => void, ) { return ( - + toggleSelection(draft.unique_key)} + checked={selectedIds.has(row.spot.unique_key)} + onChange={() => toggleSelection(row.spot.unique_key)} className='rounded' /> @@ -475,9 +561,9 @@ function commentRow(
    - {draft.type === 'PR' && } - {draft.type === 'ISSUE' && } - {draft.type === 'REDDIT' && ( + {row.spot.type === 'PR' && } + {row.spot.type === 'ISSUE' && } + {row.spot.type === 'REDDIT' && ( Reddit - {isGitHubDraft(draft) && ( + {isGitHubDraft(row.spot) && ( <> - #{draft.number} + #{row.spot.number} - {draft.slug} + {row.spot.slug} )} - {isRedditDraft(draft) && ( + {isRedditDraft(row.spot) && ( - r/{draft.subreddit} + r/{row.spot.subreddit} )}
    - {draft.linkCount > 0 && } - {draft.imageCount > 0 && } - {draft.codeCount > 0 && } - - + {row.latestDraft.stats.links.length > 0 && ( + + )} + {row.latestDraft.stats.images.length > 0 && ( + + )} + {row.latestDraft.stats.codeBlocks.length > 0 && ( + + )} + +
    {/* Title */} {/* Draft */}
    - {draft.content.substring(0, 100)}… + {row.latestDraft.content.substring(0, 100)}…
    From 613acc0acea5885c62a8b36f46c7dd80a95e15ff Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 19:25:05 -0700 Subject: [PATCH 060/109] Add sent/unsent icons. --- browser-extension/tests/playground/claude.tsx | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index a6b32d0..7d88826 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -2,23 +2,32 @@ import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' import { cva, type VariantProps } from 'class-variance-authority' -import { Clock, Code, Filter, Image, Link, Search, TextSelect } from 'lucide-react' +import { + Clock, + Code, + Filter, + Image, + Link, + MailCheck, + MessageSquareDashed, + Search, + TextSelect, +} from 'lucide-react' import { useMemo, useState } from 'react' import type { CommentSpot } from '@/lib/enhancer' import type { DraftStats } from '@/lib/enhancers/draftStats' // CVA configuration for stat badges const statBadge = cva('inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs', { - defaultVariants: { - type: 'text', - }, variants: { type: { code: 'bg-pink-50 text-pink-700', image: 'bg-purple-50 text-purple-700', link: 'bg-blue-50 text-blue-700', + sent: 'bg-green-50 text-green-700', text: 'bg-gray-50 text-gray-700', time: 'bg-gray-50 text-gray-700', + unsent: 'bg-amber-100 text-amber-700', }, }, }) @@ -28,14 +37,16 @@ const typeIcons = { code: Code, image: Image, link: Link, + sent: MailCheck, text: TextSelect, time: Clock, + unsent: MessageSquareDashed, } as const // StatBadge component type BadgeProps = VariantProps & { - text: number | string type: keyof typeof typeIcons + text?: number | string } const Badge = ({ text, type }: BadgeProps) => { @@ -43,7 +54,7 @@ const Badge = ({ text, type }: BadgeProps) => { return ( - {text} + {text || type} ) } @@ -131,7 +142,7 @@ const generateMockDrafts = (): CommentTableRow[] => [ spot: { number: 1234, slug: 'microsoft/vscode', - title: 'Fix memory leak in extension host', + title: "Fix memory leak in extension host (why is this so hard! It's been months!)", type: 'PR', unique_key: '1', } satisfies GitHubSpot, @@ -501,7 +512,7 @@ export const ClaudePrototype = () => { onChange={(e) => setHasLinkFilter(e.target.checked)} className='rounded' /> - + @@ -602,10 +613,11 @@ function commentRow( {/* Title */} -
    - + {/* Draft */}
    From 928618b0290a15d182d2048bf75982084e8e0809 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 19:45:39 -0700 Subject: [PATCH 061/109] Show archive status. --- browser-extension/tests/playground/claude.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 7d88826..e2c23fd 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -3,6 +3,7 @@ import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' import { cva, type VariantProps } from 'class-variance-authority' import { + Archive, Clock, Code, Filter, @@ -21,6 +22,7 @@ import type { DraftStats } from '@/lib/enhancers/draftStats' const statBadge = cva('inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs', { variants: { type: { + archived: 'bg-gray-50 text-yellow-700', code: 'bg-pink-50 text-pink-700', image: 'bg-purple-50 text-purple-700', link: 'bg-blue-50 text-blue-700', @@ -34,6 +36,7 @@ const statBadge = cva('inline-flex items-center gap-1 px-1.5 py-0.5 rounded text // Map types to their icons const typeIcons = { + archived: Archive, code: Code, image: Image, link: Link, @@ -618,6 +621,7 @@ function commentRow( {row.spot.title} + {row.isArchived && }
    {/* Draft */}
    From f120ca986b7a87037ed3b1e47bd12ad62ce4c997 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 19:54:22 -0700 Subject: [PATCH 062/109] Also show the open / not open status. --- browser-extension/tests/playground/claude.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index e2c23fd..79fdb07 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -11,6 +11,7 @@ import { Link, MailCheck, MessageSquareDashed, + Monitor, Search, TextSelect, } from 'lucide-react' @@ -26,6 +27,7 @@ const statBadge = cva('inline-flex items-center gap-1 px-1.5 py-0.5 rounded text code: 'bg-pink-50 text-pink-700', image: 'bg-purple-50 text-purple-700', link: 'bg-blue-50 text-blue-700', + open: 'bg-cyan-50 text-cyan-700', sent: 'bg-green-50 text-green-700', text: 'bg-gray-50 text-gray-700', time: 'bg-gray-50 text-gray-700', @@ -40,6 +42,7 @@ const typeIcons = { code: Code, image: Image, link: Link, + open: Monitor, sent: MailCheck, text: TextSelect, time: Clock, @@ -612,6 +615,7 @@ function commentRow( )} + {row.isOpenTab && }
    From d889bc1580c083ccc27e3c1cc00edff9aad1577a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 21:00:21 -0700 Subject: [PATCH 063/109] Prepare to make a multiselect control. --- browser-extension/tests/playground/claude.tsx | 110 ++++++++++-------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 79fdb07..8d2b925 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -20,25 +20,30 @@ import type { CommentSpot } from '@/lib/enhancer' import type { DraftStats } from '@/lib/enhancers/draftStats' // CVA configuration for stat badges -const statBadge = cva('inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs', { - variants: { - type: { - archived: 'bg-gray-50 text-yellow-700', - code: 'bg-pink-50 text-pink-700', - image: 'bg-purple-50 text-purple-700', - link: 'bg-blue-50 text-blue-700', - open: 'bg-cyan-50 text-cyan-700', - sent: 'bg-green-50 text-green-700', - text: 'bg-gray-50 text-gray-700', - time: 'bg-gray-50 text-gray-700', - unsent: 'bg-amber-100 text-amber-700', +const statBadge = cva( + 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal tracking-normal', + { + variants: { + type: { + archived: 'bg-gray-50 text-yellow-700', + blank: 'bg-transparent text-gray-700', + code: 'bg-pink-50 text-pink-700', + image: 'bg-purple-50 text-purple-700', + link: 'bg-blue-50 text-blue-700', + open: 'bg-cyan-50 text-cyan-700', + sent: 'bg-green-50 text-green-700', + text: 'bg-gray-50 text-gray-700', + time: 'bg-gray-50 text-gray-700', + unsent: 'bg-amber-100 text-amber-700', + }, }, }, -}) +) // Map types to their icons const typeIcons = { archived: Archive, + blank: Archive, code: Code, image: Image, link: Link, @@ -59,7 +64,7 @@ const Badge = ({ text, type }: BadgeProps) => { const Icon = typeIcons[type] return ( - + {type === 'blank' || } {text || type} ) @@ -306,6 +311,7 @@ export const ClaudePrototype = () => { const [hasCodeFilter, setHasCodeFilter] = useState(false) const [hasImageFilter, setHasImageFilter] = useState(false) const [hasLinkFilter, setHasLinkFilter] = useState(false) + const [sentFilter, setSentFilter] = useState<'all' | 'sent' | 'unsent'>('all') const [searchQuery, setSearchQuery] = useState('') const [sortBy, _setSortBy] = useState('edited-newest') const [showFilters, setShowFilters] = useState(false) @@ -321,6 +327,9 @@ export const ClaudePrototype = () => { if (hasLinkFilter) { filtered = filtered.filter((d) => d.latestDraft.stats.links.length > 0) } + if (sentFilter !== 'all') { + filtered = filtered.filter((d) => (sentFilter === 'sent' ? d.isSent : !d.isSent)) + } if (searchQuery) { const query = searchQuery.toLowerCase() filtered = filtered.filter((d) => @@ -339,7 +348,7 @@ export const ClaudePrototype = () => { break } return filtered - }, [drafts, hasCodeFilter, hasImageFilter, hasLinkFilter, searchQuery, sortBy]) + }, [drafts, hasCodeFilter, hasImageFilter, hasLinkFilter, sentFilter, searchQuery, sortBy]) const toggleSelection = (id: string) => { const newSelected = new Set(selectedIds) @@ -399,7 +408,7 @@ export const ClaudePrototype = () => { if ( filteredDrafts.length === 0 && - (searchQuery || hasCodeFilter || hasImageFilter || hasLinkFilter) + (searchQuery || hasCodeFilter || hasImageFilter || hasLinkFilter || sentFilter !== 'all') ) { return (
    @@ -428,6 +437,7 @@ export const ClaudePrototype = () => { setHasCodeFilter(false) setHasImageFilter(false) setHasLinkFilter(false) + setSentFilter('all') setSearchQuery('') }} className='text-blue-600 hover:underline' @@ -483,10 +493,7 @@ export const ClaudePrototype = () => { className='rounded' /> - +
    @@ -510,34 +517,41 @@ export const ClaudePrototype = () => {
    {showFilters && (
    -
    - - - +
    +
    + + + +
    +
    + + + +
    )} From 92fb56d47af8ed8e2208593c7b75b20667f99f64 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 21:23:36 -0700 Subject: [PATCH 064/109] Refactor our multiple booleans into a combined `FilterState`. --- browser-extension/tests/playground/claude.tsx | 165 ++++++++++-------- 1 file changed, 92 insertions(+), 73 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 8d2b925..65dff4e 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -19,6 +19,14 @@ import { useMemo, useState } from 'react' import type { CommentSpot } from '@/lib/enhancer' import type { DraftStats } from '@/lib/enhancers/draftStats' +interface FilterState { + hasLink: boolean + hasImage: boolean + hasCode: boolean + sentFilter: 'all' | 'sent' | 'unsent' + searchQuery: string +} + // CVA configuration for stat badges const statBadge = cva( 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal tracking-normal', @@ -308,47 +316,45 @@ const timeAgo = (date: Date | number) => { export const ClaudePrototype = () => { const [drafts] = useState(generateMockDrafts()) const [selectedIds, setSelectedIds] = useState(new Set()) - const [hasCodeFilter, setHasCodeFilter] = useState(false) - const [hasImageFilter, setHasImageFilter] = useState(false) - const [hasLinkFilter, setHasLinkFilter] = useState(false) - const [sentFilter, setSentFilter] = useState<'all' | 'sent' | 'unsent'>('all') - const [searchQuery, setSearchQuery] = useState('') - const [sortBy, _setSortBy] = useState('edited-newest') + const [filters, setFilters] = useState({ + hasCode: false, + hasImage: false, + hasLink: false, + searchQuery: '', + sentFilter: 'all', + }) + + const updateFilter = (key: K, value: FilterState[K]) => { + setFilters((prev) => ({ ...prev, [key]: value })) + } const [showFilters, setShowFilters] = useState(false) const filteredDrafts = useMemo(() => { let filtered = [...drafts] - if (hasCodeFilter) { + if (filters.hasCode) { filtered = filtered.filter((d) => d.latestDraft.stats.codeBlocks.length > 0) } - if (hasImageFilter) { + if (filters.hasImage) { filtered = filtered.filter((d) => d.latestDraft.stats.images.length > 0) } - if (hasLinkFilter) { + if (filters.hasLink) { filtered = filtered.filter((d) => d.latestDraft.stats.links.length > 0) } - if (sentFilter !== 'all') { - filtered = filtered.filter((d) => (sentFilter === 'sent' ? d.isSent : !d.isSent)) + if (filters.sentFilter !== 'all') { + filtered = filtered.filter((d) => (filters.sentFilter === 'sent' ? d.isSent : !d.isSent)) } - if (searchQuery) { - const query = searchQuery.toLowerCase() + if (filters.searchQuery) { + const query = filters.searchQuery.toLowerCase() filtered = filtered.filter((d) => [d.spot.title, d.latestDraft.content, (d.spot as any).slug, (d.spot as any).subreddit].some( (value) => value && String(value).toLowerCase().includes(query), ), ) } - // Sort - switch (sortBy) { - case 'edited-newest': - filtered.sort((a, b) => b.latestDraft.time - a.latestDraft.time) - break - case 'edited-oldest': - filtered.sort((a, b) => a.latestDraft.time - b.latestDraft.time) - break - } + // sort by newest + filtered.sort((a, b) => b.latestDraft.time - a.latestDraft.time) return filtered - }, [drafts, hasCodeFilter, hasImageFilter, hasLinkFilter, sentFilter, searchQuery, sortBy]) + }, [drafts, filters]) const toggleSelection = (id: string) => { const newSelected = new Set(selectedIds) @@ -408,7 +414,11 @@ export const ClaudePrototype = () => { if ( filteredDrafts.length === 0 && - (searchQuery || hasCodeFilter || hasImageFilter || hasLinkFilter || sentFilter !== 'all') + (filters.searchQuery || + filters.hasCode || + filters.hasImage || + filters.hasLink || + filters.sentFilter !== 'all') ) { return (
    @@ -421,8 +431,8 @@ export const ClaudePrototype = () => { setSearchQuery(e.target.value)} + value={filters.searchQuery} + onChange={(e) => updateFilter('searchQuery', e.target.value)} className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm' />
    @@ -434,11 +444,13 @@ export const ClaudePrototype = () => {
    @@ -515,46 +527,7 @@ export const ClaudePrototype = () => {
    - {showFilters && ( -
    -
    -
    - - - -
    -
    - - - -
    -
    -
    - )} + {showFilters && filterControls(filters, updateFilter)}
    @@ -569,6 +542,52 @@ export const ClaudePrototype = () => {
    ) } +function filterControls( + filters: FilterState, + updateFilter: (key: K, value: FilterState[K]) => void, +) { + return ( +
    +
    +
    + + + +
    +
    + + + +
    +
    +
    + ) +} + function commentRow( row: CommentTableRow, selectedIds: Set, From 7a28666dadd9c1d8bd18dfe170e6f39f2ab1188b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 21:50:10 -0700 Subject: [PATCH 065/109] Homerolled multi-select. --- browser-extension/tests/playground/claude.tsx | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 65dff4e..997b23b 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -44,6 +44,18 @@ const statBadge = cva( time: 'bg-gray-50 text-gray-700', unsent: 'bg-amber-100 text-amber-700', }, + clickable: { + true: 'cursor-pointer border border-transparent hover:border-blue-500 hover:border-opacity-50 transition-colors', + false: '', + }, + selected: { + true: 'border-blue-500 border-opacity-100', + false: '', + }, + }, + defaultVariants: { + clickable: false, + selected: false, }, }, ) @@ -66,15 +78,27 @@ const typeIcons = { type BadgeProps = VariantProps & { type: keyof typeof typeIcons text?: number | string + onClick?: () => void + isSelected?: boolean } -const Badge = ({ text, type }: BadgeProps) => { +const Badge = ({ text, type, onClick, isSelected }: BadgeProps) => { const Icon = typeIcons[type] + const Component = onClick ? 'button' : 'span' + return ( - + {type === 'blank' || } {text || type} - + ) } @@ -579,9 +603,22 @@ function filterControls(
    - - - + updateFilter('sentFilter', 'unsent')} + isSelected={filters.sentFilter === 'unsent'} + /> + updateFilter('sentFilter', 'all')} + isSelected={filters.sentFilter === 'all'} + /> + updateFilter('sentFilter', 'sent')} + isSelected={filters.sentFilter === 'sent'} + />
    From 6b32318024053c5b0138f819a826f78156ee7bf6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 22:00:45 -0700 Subject: [PATCH 066/109] use twMerge so that hover and selection works. --- browser-extension/tests/playground/claude.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 997b23b..21e21e0 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,6 +1,7 @@ //import { DraftStats } from '@/lib/enhancers/draftStats' import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' +import { twMerge } from 'tailwind-merge' import { cva, type VariantProps } from 'class-variance-authority' import { Archive, @@ -45,11 +46,11 @@ const statBadge = cva( unsent: 'bg-amber-100 text-amber-700', }, clickable: { - true: 'cursor-pointer border border-transparent hover:border-blue-500 hover:border-opacity-50 transition-colors', + true: 'cursor-pointer border border-transparent hover:border-opacity-50', false: '', }, selected: { - true: 'border-blue-500 border-opacity-100', + true: 'border-opacity-100', false: '', }, }, @@ -88,11 +89,11 @@ const Badge = ({ text, type, onClick, isSelected }: BadgeProps) => { return ( From 2ee2b3d9ab1c201aa7302f473ae682d1a323c80a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 22:21:41 -0700 Subject: [PATCH 067/109] another take --- browser-extension/tests/playground/claude.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 21e21e0..2eb2ead 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -46,17 +46,12 @@ const statBadge = cva( unsent: 'bg-amber-100 text-amber-700', }, clickable: { - true: 'cursor-pointer border border-transparent hover:border-opacity-50', - false: '', - }, - selected: { - true: 'border-opacity-100', + true: 'cursor-pointer border border-transparent hover:border-current', false: '', }, }, defaultVariants: { clickable: false, - selected: false, }, }, ) @@ -80,10 +75,9 @@ type BadgeProps = VariantProps & { type: keyof typeof typeIcons text?: number | string onClick?: () => void - isSelected?: boolean } -const Badge = ({ text, type, onClick, isSelected }: BadgeProps) => { +const Badge = ({ text, type, onClick }: BadgeProps) => { const Icon = typeIcons[type] const Component = onClick ? 'button' : 'span' @@ -91,8 +85,7 @@ const Badge = ({ text, type, onClick, isSelected }: BadgeProps) => { -
    +
    + {/* Sliding indicator */} +
    + updateFilter('sentFilter', 'unsent')} - isSelected={filters.sentFilter === 'unsent'} /> updateFilter('sentFilter', 'all')} - isSelected={filters.sentFilter === 'all'} /> updateFilter('sentFilter', 'sent')} - isSelected={filters.sentFilter === 'sent'} />
    From 67835d467e45b29ebe51192235f652d213b2d5ae Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 22:48:25 -0700 Subject: [PATCH 068/109] A take. --- browser-extension/tests/playground/claude.tsx | 95 +++++++++++-------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 2eb2ead..53b7098 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -26,6 +26,7 @@ interface FilterState { hasCode: boolean sentFilter: 'all' | 'sent' | 'unsent' searchQuery: string + showArchived: boolean } // CVA configuration for stat badges @@ -340,6 +341,7 @@ export const ClaudePrototype = () => { hasLink: false, searchQuery: '', sentFilter: 'all', + showArchived: false, }) const updateFilter = (key: K, value: FilterState[K]) => { @@ -358,6 +360,9 @@ export const ClaudePrototype = () => { if (filters.hasLink) { filtered = filtered.filter((d) => d.latestDraft.stats.links.length > 0) } + if (!filters.showArchived) { + filtered = filtered.filter((d) => !d.isArchived) + } if (filters.sentFilter !== 'all') { filtered = filtered.filter((d) => (filters.sentFilter === 'sent' ? d.isSent : !d.isSent)) } @@ -468,6 +473,7 @@ export const ClaudePrototype = () => { hasLink: false, searchQuery: '', sentFilter: 'all', + showArchived: true }) }} className='text-blue-600 hover:underline' @@ -561,52 +567,25 @@ export const ClaudePrototype = () => { ) } function filterControls( - filters: FilterState, + _filters: FilterState, updateFilter: (key: K, value: FilterState[K]) => void, ) { return (
    -
    - - - -
    -
    - {/* Sliding indicator */} -
    + updateFilter('showArchived', true)} /> - + updateFilter('showArchived', false)} + /> +
    +
    updateFilter('sentFilter', 'unsent')} @@ -621,6 +600,42 @@ function filterControls( onClick={() => updateFilter('sentFilter', 'sent')} />
    +
    + updateFilter('hasLink', true)} + /> + updateFilter('hasLink', false)} + /> +
    +
    + updateFilter('hasImage', true)} + /> + updateFilter('hasImage', false)} + /> +
    +
    + updateFilter('hasCode', true)} + /> + updateFilter('hasCode', false)} + /> +
    ) From faed7d56fc1a8c795c5ebe961639efc2675f792d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 22:55:42 -0700 Subject: [PATCH 069/109] Okay layout. --- browser-extension/tests/playground/claude.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 53b7098..02eee55 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -572,7 +572,7 @@ function filterControls( ) { return (
    -
    +
    updateFilter('sentFilter', 'sent')} />
    -
    +
    updateFilter('hasLink', true)} /> updateFilter('hasImage', true)} /> updateFilter('hasCode', true)} /> Date: Thu, 11 Sep 2025 23:15:49 -0700 Subject: [PATCH 070/109] Remove filtering by image/code/link. --- browser-extension/tests/playground/claude.tsx | 112 ++++++------------ 1 file changed, 36 insertions(+), 76 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 02eee55..12b560c 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,12 +1,12 @@ //import { DraftStats } from '@/lib/enhancers/draftStats' import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' -import { twMerge } from 'tailwind-merge' import { cva, type VariantProps } from 'class-variance-authority' import { Archive, Clock, Code, + EyeOff, Filter, Image, Link, @@ -17,13 +17,11 @@ import { TextSelect, } from 'lucide-react' import { useMemo, useState } from 'react' +import { twMerge } from 'tailwind-merge' import type { CommentSpot } from '@/lib/enhancer' import type { DraftStats } from '@/lib/enhancers/draftStats' interface FilterState { - hasLink: boolean - hasImage: boolean - hasCode: boolean sentFilter: 'all' | 'sent' | 'unsent' searchQuery: string showArchived: boolean @@ -33,11 +31,23 @@ interface FilterState { const statBadge = cva( 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal tracking-normal', { + defaultVariants: { + clickable: false, + }, variants: { + clickable: { + false: '', + true: 'cursor-pointer border border-transparent hover:border-current border-dashed', + }, + selected: { + false: '', + true: 'border-solid border-current', + }, type: { archived: 'bg-gray-50 text-yellow-700', blank: 'bg-transparent text-gray-700', code: 'bg-pink-50 text-pink-700', + hideArchived: 'bg-transparent text-gray-700', image: 'bg-purple-50 text-purple-700', link: 'bg-blue-50 text-blue-700', open: 'bg-cyan-50 text-cyan-700', @@ -46,13 +56,6 @@ const statBadge = cva( time: 'bg-gray-50 text-gray-700', unsent: 'bg-amber-100 text-amber-700', }, - clickable: { - true: 'cursor-pointer border border-transparent hover:border-current', - false: '', - }, - }, - defaultVariants: { - clickable: false, }, }, ) @@ -62,6 +65,7 @@ const typeIcons = { archived: Archive, blank: Archive, code: Code, + hideArchived: EyeOff, image: Image, link: Link, open: Monitor, @@ -76,18 +80,22 @@ type BadgeProps = VariantProps & { type: keyof typeof typeIcons text?: number | string onClick?: () => void + selected?: boolean } -const Badge = ({ text, type, onClick }: BadgeProps) => { +const Badge = ({ text, type, onClick, selected }: BadgeProps) => { const Icon = typeIcons[type] const Component = onClick ? 'button' : 'span' return ( @@ -336,9 +344,6 @@ export const ClaudePrototype = () => { const [drafts] = useState(generateMockDrafts()) const [selectedIds, setSelectedIds] = useState(new Set()) const [filters, setFilters] = useState({ - hasCode: false, - hasImage: false, - hasLink: false, searchQuery: '', sentFilter: 'all', showArchived: false, @@ -351,15 +356,6 @@ export const ClaudePrototype = () => { const filteredDrafts = useMemo(() => { let filtered = [...drafts] - if (filters.hasCode) { - filtered = filtered.filter((d) => d.latestDraft.stats.codeBlocks.length > 0) - } - if (filters.hasImage) { - filtered = filtered.filter((d) => d.latestDraft.stats.images.length > 0) - } - if (filters.hasLink) { - filtered = filtered.filter((d) => d.latestDraft.stats.links.length > 0) - } if (!filters.showArchived) { filtered = filtered.filter((d) => !d.isArchived) } @@ -435,14 +431,7 @@ export const ClaudePrototype = () => { ) } - if ( - filteredDrafts.length === 0 && - (filters.searchQuery || - filters.hasCode || - filters.hasImage || - filters.hasLink || - filters.sentFilter !== 'all') - ) { + if (filteredDrafts.length === 0 && (filters.searchQuery || filters.sentFilter !== 'all')) { return (
    @@ -468,12 +457,9 @@ export const ClaudePrototype = () => { type='button' onClick={() => { setFilters({ - hasCode: false, - hasImage: false, - hasLink: false, searchQuery: '', sentFilter: 'all', - showArchived: true + showArchived: true, }) }} className='text-blue-600 hover:underline' @@ -567,7 +553,7 @@ export const ClaudePrototype = () => { ) } function filterControls( - _filters: FilterState, + filters: FilterState, updateFilter: (key: K, value: FilterState[K]) => void, ) { return ( @@ -576,63 +562,37 @@ function filterControls(
    updateFilter('showArchived', true)} /> updateFilter('showArchived', false)} />
    updateFilter('sentFilter', 'unsent')} /> updateFilter('sentFilter', 'all')} /> updateFilter('sentFilter', 'sent')} />
    -
    - updateFilter('hasLink', true)} - /> - updateFilter('hasLink', false)} - /> -
    -
    - updateFilter('hasImage', true)} - /> - updateFilter('hasImage', false)} - /> -
    -
    - updateFilter('hasCode', true)} - /> - updateFilter('hasCode', false)} - /> -
    ) From 0920513949efdbd62784872363406ce7dea5adce Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 11 Sep 2025 23:36:11 -0700 Subject: [PATCH 071/109] Inline the filter options. --- browser-extension/tests/playground/claude.tsx | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 12b560c..dec7248 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -528,16 +528,8 @@ export const ClaudePrototype = () => { className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' />
    - + {filterControls(filters, updateFilter)}
    - {showFilters && filterControls(filters, updateFilter)}
    @@ -557,44 +549,42 @@ function filterControls( updateFilter: (key: K, value: FilterState[K]) => void, ) { return ( -
    -
    -
    - updateFilter('showArchived', true)} - /> - updateFilter('showArchived', false)} - /> -
    -
    - updateFilter('sentFilter', 'unsent')} - /> - updateFilter('sentFilter', 'all')} - /> - updateFilter('sentFilter', 'sent')} - /> -
    + <> +
    + updateFilter('showArchived', true)} + /> + updateFilter('showArchived', false)} + />
    -
    +
    + updateFilter('sentFilter', 'unsent')} + /> + updateFilter('sentFilter', 'all')} + /> + updateFilter('sentFilter', 'sent')} + /> +
    + ) } From ec3135b97f86dedce6f62f30028285b3a994f1eb Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 09:47:53 -0700 Subject: [PATCH 072/109] Remove the archive controls. --- browser-extension/tests/playground/claude.tsx | 68 ++++++------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index dec7248..822aa55 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -29,7 +29,7 @@ interface FilterState { // CVA configuration for stat badges const statBadge = cva( - 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal tracking-normal', + 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal', { defaultVariants: { clickable: false, @@ -515,7 +515,7 @@ export const ClaudePrototype = () => { className='rounded' /> - +
    @@ -528,7 +528,26 @@ export const ClaudePrototype = () => { className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' />
    - {filterControls(filters, updateFilter)} +
    + updateFilter('sentFilter', 'unsent')} + /> + updateFilter('sentFilter', 'all')} + /> + updateFilter('sentFilter', 'sent')} + /> +
    @@ -544,49 +563,6 @@ export const ClaudePrototype = () => {
    ) } -function filterControls( - filters: FilterState, - updateFilter: (key: K, value: FilterState[K]) => void, -) { - return ( - <> -
    - updateFilter('showArchived', true)} - /> - updateFilter('showArchived', false)} - /> -
    -
    - updateFilter('sentFilter', 'unsent')} - /> - updateFilter('sentFilter', 'all')} - /> - updateFilter('sentFilter', 'sent')} - /> -
    - - ) -} function commentRow( row: CommentTableRow, From 3aaa1da2b13160c516611fe134668be7d068abd6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 10:42:34 -0700 Subject: [PATCH 073/109] One step. --- browser-extension/tests/playground/claude.tsx | 92 ++++++++++++------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 822aa55..3228733 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -7,16 +7,16 @@ import { Clock, Code, EyeOff, - Filter, Image, Link, + LucideProps, MailCheck, MessageSquareDashed, Monitor, Search, TextSelect, } from 'lucide-react' -import { useMemo, useState } from 'react' +import react, { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' import type { CommentSpot } from '@/lib/enhancer' import type { DraftStats } from '@/lib/enhancers/draftStats' @@ -79,29 +79,57 @@ const typeIcons = { type BadgeProps = VariantProps & { type: keyof typeof typeIcons text?: number | string - onClick?: () => void - selected?: boolean } -const Badge = ({ text, type, onClick, selected }: BadgeProps) => { +const Badge = ({ text, type }: BadgeProps) => { const Icon = typeIcons[type] - const Component = onClick ? 'button' : 'span' - return ( - {type === 'blank' || } {text || type} - + + ) +} + + +interface Segment { + text?: string + type: keyof typeof typeIcons + onClick: () => void + selected: boolean +} +interface MultiSegmentProps { + segments: Segment[] +} + +const MultiSegment = ({ segments }: MultiSegmentProps) => { + return ( +
    + {segments.map((segment, index) => { + const Icon = typeIcons[segment.type] + return ( + + ) + })} +
    ) } @@ -529,24 +557,26 @@ export const ClaudePrototype = () => { />
    - updateFilter('sentFilter', 'unsent')} - /> - updateFilter('sentFilter', 'all')} - /> - updateFilter('sentFilter', 'sent')} - /> + updateFilter('sentFilter', 'unsent') + }, + { + type: 'blank', + text: 'both', + selected: filters.sentFilter === 'all', + onClick: () => updateFilter('sentFilter', 'all') + }, + { + type: 'sent', + text: ' ', + selected: filters.sentFilter === 'sent', + onClick: () => updateFilter('sentFilter', 'sent') + } + ]} />
    From 6c78fa44f89798608e23dc274eb732623e0db8fc Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 10:48:39 -0700 Subject: [PATCH 074/109] Update. --- browser-extension/tests/playground/claude.tsx | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 3228733..8d98b53 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -101,14 +101,15 @@ const Badge = ({ text, type }: BadgeProps) => { interface Segment { text?: string type: keyof typeof typeIcons - onClick: () => void - selected: boolean + value: string } interface MultiSegmentProps { segments: Segment[] + value: string + onValueChange: (value: string) => void } -const MultiSegment = ({ segments }: MultiSegmentProps) => { +const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps) => { return (
    {segments.map((segment, index) => { @@ -118,10 +119,10 @@ const MultiSegment = ({ segments }: MultiSegmentProps) => { key={index} className={statBadge({ clickable: true, - selected: segment.selected, + selected: value === segment.value, type: segment.type, })} - onClick={segment.onClick} + onClick={() => onValueChange(segment.value)} type="button" > {segment.type === 'blank' || } @@ -557,26 +558,26 @@ export const ClaudePrototype = () => { />
    - updateFilter('sentFilter', 'unsent') - }, - { - type: 'blank', - text: 'both', - selected: filters.sentFilter === 'all', - onClick: () => updateFilter('sentFilter', 'all') - }, - { - type: 'sent', - text: ' ', - selected: filters.sentFilter === 'sent', - onClick: () => updateFilter('sentFilter', 'sent') - } - ]} /> + updateFilter('sentFilter', value as 'all' | 'sent' | 'unsent')} + segments={[ + { + type: 'unsent', + text: ' ', + value: 'unsent' + }, + { + type: 'blank', + text: 'both', + value: 'all' + }, + { + type: 'sent', + text: ' ', + value: 'sent' + } + ]} />
    From e35167ebfd41a36b095e08856a809d401bdca142 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 10:54:47 -0700 Subject: [PATCH 075/109] Make the border solidity work. --- browser-extension/tests/playground/claude.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 8d98b53..bc2bd9b 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -41,7 +41,7 @@ const statBadge = cva( }, selected: { false: '', - true: 'border-solid border-current', + true: '!border-solid !border-current', }, type: { archived: 'bg-gray-50 text-yellow-700', @@ -564,7 +564,7 @@ export const ClaudePrototype = () => { segments={[ { type: 'unsent', - text: ' ', + text: '', value: 'unsent' }, { @@ -574,7 +574,7 @@ export const ClaudePrototype = () => { }, { type: 'sent', - text: ' ', + text: '', value: 'sent' } ]} /> From c16b60c68c9b36ca474647478a4d6e34ad108052 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 10:57:39 -0700 Subject: [PATCH 076/109] Fix border roundedness. --- browser-extension/tests/playground/claude.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index bc2bd9b..4a267a5 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -114,14 +114,25 @@ const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps) =>
    {segments.map((segment, index) => { const Icon = typeIcons[segment.type] + const isFirst = index === 0 + const isLast = index === segments.length - 1 + + const roundedClasses = isFirst && isLast + ? '' + : isFirst + ? '!rounded-r-none' + : isLast + ? '!rounded-l-none' + : '!rounded-none' + return (
    From 7238fcb6140676ce357037fdb2209e9e9a894241 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 11:18:35 -0700 Subject: [PATCH 079/109] Better type checking. --- browser-extension/tests/playground/claude.tsx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 66d51db..570ff94 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -19,7 +19,7 @@ import type { CommentSpot } from '@/lib/enhancer' import type { DraftStats } from '@/lib/enhancers/draftStats' interface FilterState { - sentFilter: 'all' | 'sent' | 'unsent' + sentFilter: 'both' | 'sent' | 'unsent' searchQuery: string showArchived: boolean } @@ -94,18 +94,18 @@ const Badge = ({ text, type }: BadgeProps) => { ) } -interface Segment { +interface Segment { text?: string type: keyof typeof typeIcons - value: string + value: T } -interface MultiSegmentProps { - segments: Segment[] - value: string - onValueChange: (value: string) => void +interface MultiSegmentProps { + segments: Segment[] + value: T + onValueChange: (value: T) => void } -const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps) => { +const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps) => { return (
    {segments.map((segment, index) => { @@ -124,7 +124,7 @@ const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps) => return ( - - - -
    - )} - +
    + {/* Bulk actions bar - floating popup */} + {selectedIds.size > 0 && ( +
    + {selectedIds.size} selected + + + + +
    + )} {/* Table */}
    @@ -673,7 +668,7 @@ function commentRow( {row.spot.title} - {row.isArchived && } + {row.isTrashed && }
    {/* Draft */}
    diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 41933ce..acb03d3 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -13,7 +13,7 @@ const MODES = { type Mode = keyof typeof MODES const App = () => { - const [activeComponent, setActiveComponent] = useState('replica') + const [activeComponent, setActiveComponent] = useState('claude') return (
    @@ -30,11 +30,10 @@ const App = () => { key={mode} type='button' onClick={() => setActiveComponent(mode as Mode)} - className={`px-3 py-2 rounded text-sm font-medium transition-colors ${ - activeComponent === mode - ? 'bg-blue-600 text-white' - : 'bg-gray-100 text-gray-700 hover:bg-gray-200' - }`} + className={`px-3 py-2 rounded text-sm font-medium transition-colors ${activeComponent === mode + ? 'bg-blue-600 text-white' + : 'bg-gray-100 text-gray-700 hover:bg-gray-200' + }`} > {config.label} diff --git a/browser-extension/tests/playground/style.css b/browser-extension/tests/playground/style.css index 0659880..e6587c5 100644 --- a/browser-extension/tests/playground/style.css +++ b/browser-extension/tests/playground/style.css @@ -17,13 +17,12 @@ body { /* Popup simulator frame */ .popup-frame { width: var(--popup-width); - padding: 15px; + height: var(--popup-height); font-size: 14px; line-height: 1.4; background: white; border: 1px solid #e2e8f0; - border-radius: 8px; + border-radius: 0px; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); margin: 0 auto; - text-align: left; } From 377b2e02cb3e9289e4a69374908a0bf886fa1b23 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 12:13:31 -0700 Subject: [PATCH 081/109] Improve the search box. --- browser-extension/tests/playground/claude.tsx | 58 ++++++++++++++----- .../tests/playground/playground.tsx | 9 +-- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 7b50a41..8dbb78f 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -11,7 +11,7 @@ import { Monitor, Search, TextSelect, - Trash, + Trash2, } from 'lucide-react' import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' @@ -41,16 +41,16 @@ const statBadge = cva( true: '!border-solid !border-current', }, type: { - trashed: 'bg-gray-50 text-yellow-700', blank: 'bg-transparent text-gray-700', code: 'bg-pink-50 text-pink-700', - hideArchived: 'bg-transparent text-gray-700', + hideTrashed: 'bg-transparent text-gray-700', image: 'bg-purple-50 text-purple-700', link: 'bg-blue-50 text-blue-700', open: 'bg-cyan-50 text-cyan-700', sent: 'bg-green-50 text-green-700', text: 'bg-gray-50 text-gray-700', time: 'bg-gray-50 text-gray-700', + trashed: 'bg-gray-50 text-yellow-700', unsent: 'bg-amber-100 text-amber-700', }, }, @@ -59,16 +59,16 @@ const statBadge = cva( // Map types to their icons const typeIcons = { - trashed: Trash, blank: Code, code: Code, - hideArchived: EyeOff, + hideTrashed: EyeOff, image: Image, link: Link, open: Monitor, sent: MailCheck, text: TextSelect, time: Clock, + trashed: Trash2, unsent: MessageSquareDashed, } as const @@ -198,9 +198,9 @@ const isRedditDraft = (spot: GitHubOrReddit): spot is RedditSpot => { const generateMockDrafts = (): CommentTableRow[] => [ { - isTrashed: false, isOpenTab: true, isSent: false, + isTrashed: false, latestDraft: { content: 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', @@ -231,9 +231,9 @@ const generateMockDrafts = (): CommentTableRow[] => [ } satisfies GitHubSpot, }, { - isTrashed: false, isOpenTab: false, isSent: false, + isTrashed: false, latestDraft: { content: "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", @@ -258,9 +258,9 @@ const generateMockDrafts = (): CommentTableRow[] => [ } satisfies RedditSpot, }, { - isTrashed: false, isOpenTab: true, isSent: false, + isTrashed: false, latestDraft: { content: "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", @@ -281,9 +281,9 @@ const generateMockDrafts = (): CommentTableRow[] => [ } satisfies GitHubSpot, }, { - isTrashed: false, isOpenTab: false, isSent: true, + isTrashed: false, latestDraft: { content: 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', @@ -316,9 +316,9 @@ const generateMockDrafts = (): CommentTableRow[] => [ } satisfies GitHubSpot, }, { - isTrashed: true, isOpenTab: true, isSent: false, + isTrashed: true, latestDraft: { content: 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', @@ -470,7 +470,7 @@ export const ClaudePrototype = () => { if (filteredDrafts.length === 0 && (filters.searchQuery || filters.sentFilter !== 'both')) { return (
    -
    +
    {/* Keep the header controls visible */}
    {/* Search */} @@ -481,7 +481,7 @@ export const ClaudePrototype = () => { placeholder='Search drafts...' value={filters.searchQuery} onChange={(e) => updateFilter('searchQuery', e.target.value)} - className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm' + className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-sm text-sm font-normal' />
    @@ -535,7 +535,7 @@ export const ClaudePrototype = () => { - + { placeholder='Search drafts...' value={filters.searchQuery} onChange={(e) => updateFilter('searchQuery', e.target.value)} - className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' + className='w-full pl-9 pr-3 h-5 border border-gray-300 rounded-sm text-sm font-normal focus:outline-none focus:border-blue-500' />
    -
    +
    + + + + value={filters.sentFilter} onValueChange={(value) => updateFilter('sentFilter', value)} @@ -581,6 +592,23 @@ export const ClaudePrototype = () => { }, ]} /> + {/* + + value={filters.showTrashed} + onValueChange={(value) => updateFilter('showTrashed', value)} + segments={[ + { + text: 'show trashed', + type: 'trashed', + value: true, + }, + { + text: 'hide trashed', + type: 'hideTrashed', + value: false + }, + ]} + /> */}
    diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index acb03d3..0351164 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -30,10 +30,11 @@ const App = () => { key={mode} type='button' onClick={() => setActiveComponent(mode as Mode)} - className={`px-3 py-2 rounded text-sm font-medium transition-colors ${activeComponent === mode - ? 'bg-blue-600 text-white' - : 'bg-gray-100 text-gray-700 hover:bg-gray-200' - }`} + className={`px-3 py-2 rounded text-sm font-medium transition-colors ${ + activeComponent === mode + ? 'bg-blue-600 text-white' + : 'bg-gray-100 text-gray-700 hover:bg-gray-200' + }`} > {config.label} From 7a2f78037a26b0824037e11b8d56e777176ccdd3 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 12:32:46 -0700 Subject: [PATCH 082/109] Cleanup empty states. --- browser-extension/tests/playground/claude.tsx | 113 ++++++++---------- 1 file changed, 47 insertions(+), 66 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 8dbb78f..16360ff 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -443,67 +443,25 @@ export const ClaudePrototype = () => { } } - // Empty states - if (drafts.length === 0) { - return ( -
    -
    -

    No comments open

    -

    - Your drafts will appear here when you start typing in comment boxes across GitHub and - Reddit. -

    -
    - - · - -
    -
    -
    - ) + const clearFilters = () => { + setFilters({ + searchQuery: '', + sentFilter: 'both', + showTrashed: true, + }) } - if (filteredDrafts.length === 0 && (filters.searchQuery || filters.sentFilter !== 'both')) { - return ( -
    -
    - {/* Keep the header controls visible */} -
    - {/* Search */} -
    - - updateFilter('searchQuery', e.target.value)} - className='w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-sm text-sm font-normal' - /> -
    -
    -
    + const getTableBody = () => { + if (drafts.length === 0) { + return + } -
    -

    No matches found

    - -
    -
    + if (filteredDrafts.length === 0 && (filters.searchQuery || filters.sentFilter !== 'both')) { + return + } + + return filteredDrafts.map((row) => + commentRow(row, selectedIds, toggleSelection, handleOpen, handleTrash), ) } @@ -530,7 +488,7 @@ export const ClaudePrototype = () => { {/* Table */}
    - +
    @@ -550,13 +508,13 @@ export const ClaudePrototype = () => {
    - + updateFilter('searchQuery', e.target.value)} - className='w-full pl-9 pr-3 h-5 border border-gray-300 rounded-sm text-sm font-normal focus:outline-none focus:border-blue-500' + className='w-full pl-5 pr-3 h-5 border border-gray-300 rounded-sm text-sm font-normal focus:outline-none focus:border-blue-500' />
    @@ -615,11 +573,7 @@ export const ClaudePrototype = () => { -
    - {filteredDrafts.map((row) => - commentRow(row, selectedIds, toggleSelection, handleOpen, handleTrash), - )} - + {getTableBody()}
    @@ -707,3 +661,30 @@ function commentRow( ) } + +const EmptyState = () => ( +
    +

    No comments open

    +

    + Your drafts will appear here when you start typing in comment boxes across GitHub and Reddit. +

    +
    + + · + +
    +
    +) + +const NoMatchesState = ({ onClearFilters }: { onClearFilters: () => void }) => ( +
    +

    No matches found

    + +
    +) From 3f9111cd9b3d276fb21b180723c131145f57f5b9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 13:26:06 -0700 Subject: [PATCH 083/109] Trash that's good enough for the foreseeable future. --- browser-extension/tests/playground/claude.tsx | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 16360ff..108f00b 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -3,6 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority' import { Clock, Code, + Eye, EyeOff, Image, Link, @@ -518,17 +519,24 @@ export const ClaudePrototype = () => { />
    - updateFilter('showTrashed', !filters.showTrashed)} className={twMerge( statBadge({ - type: 'trashed', + clickable: true, + type: filters.showTrashed ? 'trashed' : 'hideTrashed', }), 'border', )} > - - + {filters.showTrashed ? ( + + ) : ( + + )} + value={filters.sentFilter} onValueChange={(value) => updateFilter('sentFilter', value)} @@ -550,23 +558,6 @@ export const ClaudePrototype = () => { }, ]} /> - {/* - - value={filters.showTrashed} - onValueChange={(value) => updateFilter('showTrashed', value)} - segments={[ - { - text: 'show trashed', - type: 'trashed', - value: true, - }, - { - text: 'hide trashed', - type: 'hideTrashed', - value: false - }, - ]} - /> */}
    From 666c3abe4e8f9779bad6516c086f383ef8a829b3 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 13:30:31 -0700 Subject: [PATCH 084/109] Add a settings button. --- browser-extension/tests/playground/claude.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 108f00b..e5e064c 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -11,6 +11,7 @@ import { MessageSquareDashed, Monitor, Search, + Settings, TextSelect, Trash2, } from 'lucide-react' @@ -49,6 +50,7 @@ const statBadge = cva( link: 'bg-blue-50 text-blue-700', open: 'bg-cyan-50 text-cyan-700', sent: 'bg-green-50 text-green-700', + settings: 'bg-gray-50 text-gray-700', text: 'bg-gray-50 text-gray-700', time: 'bg-gray-50 text-gray-700', trashed: 'bg-gray-50 text-yellow-700', @@ -67,6 +69,7 @@ const typeIcons = { link: Link, open: Monitor, sent: MailCheck, + settings: Settings, text: TextSelect, time: Clock, trashed: Trash2, @@ -558,6 +561,18 @@ export const ClaudePrototype = () => { }, ]} /> + From d1132b110f15bb82e58ccc51ac261cb482056566 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 13:49:13 -0700 Subject: [PATCH 085/109] Move our claude prototype to the real actual CommentSpots we're working with. --- .../github/githubIssueAddComment.tsx | 3 + .../enhancers/github/githubPRAddComment.tsx | 3 + .../tests/lib/enhancers/github.test.ts | 2 + browser-extension/tests/playground/claude.tsx | 56 +++++++------------ .../tests/playground/replica.tsx | 2 + 5 files changed, 29 insertions(+), 37 deletions(-) diff --git a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx index b35f8f9..7c75ee8 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx @@ -7,6 +7,7 @@ import { githubHighlighter } from './githubHighlighter' export interface GitHubIssueAddCommentSpot extends CommentSpot { type: 'GH_ISSUE_ADD_COMMENT' + title: string domain: string slug: string // owner/repo number: number // issue number, undefined for new issues @@ -32,10 +33,12 @@ export class GitHubIssueAddCommentEnhancer implements CommentEnhancer { "domain": "github.com", "number": 517, "slug": "diffplug/selfie", + "title": "TODO_TITLE", "type": "GH_PR_ADD_COMMENT", "unique_key": "github.com:diffplug/selfie:517", } @@ -47,6 +48,7 @@ describe('github', () => { "domain": "github.com", "number": 523, "slug": "diffplug/selfie", + "title": "TODO_TITLE", "type": "GH_ISSUE_ADD_COMMENT", "unique_key": "github.com:diffplug/selfie:523", } diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index e5e064c..ec10ad6 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -19,6 +19,8 @@ import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' import type { CommentSpot } from '@/lib/enhancer' import type { DraftStats } from '@/lib/enhancers/draftStats' +import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' +import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' interface FilterState { sentFilter: 'both' | 'sent' | 'unsent' @@ -146,30 +148,6 @@ const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps< ) } -/* -interface GitHubIssueAddCommentSpot extends CommentSpot { - type: 'GH_ISSUE_ADD_COMMENT' - domain: 'string' - slug: string // owner/repo - number: number // issue number, undefined for new issues - title: string -} -export interface GitHubPRAddCommentSpot extends CommentSpot { - type: 'GH_PR_ADD_COMMENT' // Override to narrow from string to specific union - domain: string - slug: string // owner/repo - number: number // issue/PR number, undefined for new issues and PRs - title: string -} -*/ - -interface GitHubSpot extends CommentSpot { - title: string - slug: string - number: number - type: 'PR' | 'ISSUE' -} - interface RedditSpot extends CommentSpot { title: string subreddit: string @@ -190,10 +168,10 @@ interface CommentTableRow { isTrashed: boolean } -type GitHubOrReddit = GitHubSpot | RedditSpot +type GitHubOrReddit = GitHubIssueAddCommentSpot | GitHubPRAddCommentSpot | RedditSpot -const isGitHubDraft = (spot: GitHubOrReddit): spot is GitHubSpot => { - return spot.type === 'PR' || spot.type === 'ISSUE' +const isGitHubDraft = (spot: GitHubOrReddit): spot is GitHubIssueAddCommentSpot => { + return spot.type === 'GH_PR_ADD_COMMENT' || spot.type === 'GH_ISSUE_ADD_COMMENT' } const isRedditDraft = (spot: GitHubOrReddit): spot is RedditSpot => { @@ -227,12 +205,13 @@ const generateMockDrafts = (): CommentTableRow[] => [ time: Date.now() - 1000 * 60 * 30, }, spot: { + domain: 'github.com', number: 1234, slug: 'microsoft/vscode', title: "Fix memory leak in extension host (why is this so hard! It's been months!)", - type: 'PR', + type: 'GH_PR_ADD_COMMENT', unique_key: '1', - } satisfies GitHubSpot, + } satisfies GitHubPRAddCommentSpot, }, { isOpenTab: false, @@ -277,12 +256,13 @@ const generateMockDrafts = (): CommentTableRow[] => [ time: Date.now() - 1000 * 60 * 60 * 5, }, spot: { + domain: 'github.com', number: 5678, slug: 'facebook/react', title: 'Unexpected behavior with useEffect cleanup', - type: 'ISSUE', + type: 'GH_ISSUE_ADD_COMMENT', unique_key: '3', - } satisfies GitHubSpot, + } satisfies GitHubIssueAddCommentSpot, }, { isOpenTab: false, @@ -312,12 +292,13 @@ const generateMockDrafts = (): CommentTableRow[] => [ time: Date.now() - 1000 * 60 * 60 * 24, }, spot: { + domain: 'github', number: 9012, slug: 'vercel/next.js', title: 'Update routing documentation', - type: 'PR', + type: 'GH_PR_ADD_COMMENT', unique_key: '4', - } satisfies GitHubSpot, + } satisfies GitHubPRAddCommentSpot, }, { isOpenTab: true, @@ -352,12 +333,13 @@ const generateMockDrafts = (): CommentTableRow[] => [ time: Date.now() - 1000 * 60 * 60 * 48, }, spot: { + domain: 'github.com', number: 3456, slug: 'nodejs/node', title: 'Add support for ESM in worker threads', - type: 'PR', + type: 'GH_PR_ADD_COMMENT', unique_key: '5', - } satisfies GitHubSpot, + } satisfies GitHubPRAddCommentSpot, }, ] @@ -609,8 +591,8 @@ function commentRow(
    - {row.spot.type === 'PR' && } - {row.spot.type === 'ISSUE' && } + {row.spot.type === 'GH_PR_ADD_COMMENT' && } + {row.spot.type === 'GH_ISSUE_ADD_COMMENT' && } {row.spot.type === 'REDDIT' && ( Date: Fri, 12 Sep 2025 13:52:54 -0700 Subject: [PATCH 086/109] Pull `generateMockDrafts` out of `claude.tsx` into `replicaData.tsx` --- browser-extension/tests/playground/claude.tsx | 205 +----------------- .../tests/playground/replicaData.tsx | 193 +++++++++++++++++ 2 files changed, 199 insertions(+), 199 deletions(-) create mode 100644 browser-extension/tests/playground/replicaData.tsx diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index ec10ad6..2bd93da 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -17,10 +17,12 @@ import { } from 'lucide-react' import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' -import type { CommentSpot } from '@/lib/enhancer' -import type { DraftStats } from '@/lib/enhancers/draftStats' -import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' -import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' +import { + type CommentTableRow, + generateMockDrafts, + isGitHubDraft, + isRedditDraft, +} from './replicaData' interface FilterState { sentFilter: 'both' | 'sent' | 'unsent' @@ -148,201 +150,6 @@ const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps< ) } -interface RedditSpot extends CommentSpot { - title: string - subreddit: string - type: 'REDDIT' -} - -interface Draft { - content: string - time: number - stats: DraftStats -} - -interface CommentTableRow { - spot: GitHubOrReddit - latestDraft: Draft - isOpenTab: boolean - isSent: boolean - isTrashed: boolean -} - -type GitHubOrReddit = GitHubIssueAddCommentSpot | GitHubPRAddCommentSpot | RedditSpot - -const isGitHubDraft = (spot: GitHubOrReddit): spot is GitHubIssueAddCommentSpot => { - return spot.type === 'GH_PR_ADD_COMMENT' || spot.type === 'GH_ISSUE_ADD_COMMENT' -} - -const isRedditDraft = (spot: GitHubOrReddit): spot is RedditSpot => { - return spot.type === 'REDDIT' -} - -const generateMockDrafts = (): CommentTableRow[] => [ - { - isOpenTab: true, - isSent: false, - isTrashed: false, - latestDraft: { - content: - 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', - stats: { - charCount: 245, - codeBlocks: [ - { code: 'const listener = () => {}', language: 'typescript' }, - { code: 'element.removeEventListener()', language: 'javascript' }, - { code: 'dispose()', language: 'typescript' }, - ], - images: [ - { url: 'https://example.com/image1.png' }, - { url: 'https://example.com/image2.png' }, - ], - links: [ - { text: 'Issue #1233', url: 'https://github.com/microsoft/vscode/issues/1233' }, - { text: 'Documentation', url: 'https://docs.microsoft.com' }, - ], - }, - time: Date.now() - 1000 * 60 * 30, - }, - spot: { - domain: 'github.com', - number: 1234, - slug: 'microsoft/vscode', - title: "Fix memory leak in extension host (why is this so hard! It's been months!)", - type: 'GH_PR_ADD_COMMENT', - unique_key: '1', - } satisfies GitHubPRAddCommentSpot, - }, - { - isOpenTab: false, - isSent: false, - isTrashed: false, - latestDraft: { - content: - "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", - stats: { - charCount: 180, - codeBlocks: [], - images: [], - links: [ - { - text: 'GitLens', - url: 'https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens', - }, - ], - }, - time: Date.now() - 1000 * 60 * 60 * 2, - }, - spot: { - subreddit: 'programming', - title: "Re: What's your favorite VS Code extension?", - type: 'REDDIT', - unique_key: '2', - } satisfies RedditSpot, - }, - { - isOpenTab: true, - isSent: false, - isTrashed: false, - latestDraft: { - content: - "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", - stats: { - charCount: 456, - codeBlocks: [{ code: 'useEffect(() => { /* async code */ }, [])', language: 'javascript' }], - images: [], - links: [], - }, - time: Date.now() - 1000 * 60 * 60 * 5, - }, - spot: { - domain: 'github.com', - number: 5678, - slug: 'facebook/react', - title: 'Unexpected behavior with useEffect cleanup', - type: 'GH_ISSUE_ADD_COMMENT', - unique_key: '3', - } satisfies GitHubIssueAddCommentSpot, - }, - { - isOpenTab: false, - isSent: true, - isTrashed: false, - latestDraft: { - content: - 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', - stats: { - charCount: 322, - codeBlocks: [], - images: [ - { url: 'routing-diagram.png' }, - { url: 'example-1.png' }, - { url: 'example-2.png' }, - { url: 'architecture.png' }, - ], - links: [ - { text: 'Routing docs', url: 'https://nextjs.org/docs/routing' }, - { text: 'Examples', url: 'https://github.com/vercel/next.js/tree/main/examples' }, - { - text: 'Migration guide', - url: 'https://nextjs.org/docs/app/building-your-application/upgrading', - }, - ], - }, - time: Date.now() - 1000 * 60 * 60 * 24, - }, - spot: { - domain: 'github', - number: 9012, - slug: 'vercel/next.js', - title: 'Update routing documentation', - type: 'GH_PR_ADD_COMMENT', - unique_key: '4', - } satisfies GitHubPRAddCommentSpot, - }, - { - isOpenTab: true, - isSent: false, - isTrashed: true, - latestDraft: { - content: - 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', - stats: { - charCount: 678, - codeBlocks: [ - { code: 'import { Worker } from "worker_threads"', language: 'javascript' }, - { code: 'new Worker("./worker.mjs", { type: "module" })', language: 'javascript' }, - { code: 'import { parentPort } from "worker_threads"', language: 'javascript' }, - { code: 'interface WorkerOptions { type: "module" }', language: 'typescript' }, - { code: 'await import("./dynamic-module.mjs")', language: 'javascript' }, - { code: 'export default function workerTask() {}', language: 'javascript' }, - { code: 'const result = await workerPromise', language: 'javascript' }, - ], - images: [{ alt: 'ESM Worker Architecture', url: 'worker-architecture.png' }], - links: [ - { - text: 'TSC Meeting Notes', - url: 'https://github.com/nodejs/TSC/blob/main/meetings/2023-11-01.md', - }, - { text: 'ESM Spec', url: 'https://tc39.es/ecma262/' }, - { text: 'Worker Threads docs', url: 'https://nodejs.org/api/worker_threads.html' }, - { text: 'Implementation guide', url: 'https://nodejs.org/api/esm.html' }, - { text: 'Related issue', url: 'https://github.com/nodejs/node/issues/30682' }, - ], - }, - time: Date.now() - 1000 * 60 * 60 * 48, - }, - spot: { - domain: 'github.com', - number: 3456, - slug: 'nodejs/node', - title: 'Add support for ESM in worker threads', - type: 'GH_PR_ADD_COMMENT', - unique_key: '5', - } satisfies GitHubPRAddCommentSpot, - }, -] - // Helper function for relative time const timeAgo = (date: Date | number) => { const timestamp = typeof date === 'number' ? date : date.getTime() diff --git a/browser-extension/tests/playground/replicaData.tsx b/browser-extension/tests/playground/replicaData.tsx new file mode 100644 index 0000000..d4fa676 --- /dev/null +++ b/browser-extension/tests/playground/replicaData.tsx @@ -0,0 +1,193 @@ +import type { CommentSpot } from '@/lib/enhancer' +import type { DraftStats } from '@/lib/enhancers/draftStats' +import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' +import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' + +interface RedditSpot extends CommentSpot { + title: string + subreddit: string + type: 'REDDIT' +} +interface Draft { + content: string + time: number + stats: DraftStats +} +export interface CommentTableRow { + spot: GitHubOrReddit + latestDraft: Draft + isOpenTab: boolean + isSent: boolean + isTrashed: boolean +} +type GitHubOrReddit = GitHubIssueAddCommentSpot | GitHubPRAddCommentSpot | RedditSpot +export const isGitHubDraft = (spot: GitHubOrReddit): spot is GitHubIssueAddCommentSpot => { + return spot.type === 'GH_PR_ADD_COMMENT' || spot.type === 'GH_ISSUE_ADD_COMMENT' +} +export const isRedditDraft = (spot: GitHubOrReddit): spot is RedditSpot => { + return spot.type === 'REDDIT' +} +export const generateMockDrafts = (): CommentTableRow[] => [ + { + isOpenTab: true, + isSent: false, + isTrashed: false, + latestDraft: { + content: + 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', + stats: { + charCount: 245, + codeBlocks: [ + { code: 'const listener = () => {}', language: 'typescript' }, + { code: 'element.removeEventListener()', language: 'javascript' }, + { code: 'dispose()', language: 'typescript' }, + ], + images: [ + { url: 'https://example.com/image1.png' }, + { url: 'https://example.com/image2.png' }, + ], + links: [ + { text: 'Issue #1233', url: 'https://github.com/microsoft/vscode/issues/1233' }, + { text: 'Documentation', url: 'https://docs.microsoft.com' }, + ], + }, + time: Date.now() - 1000 * 60 * 30, + }, + spot: { + domain: 'github.com', + number: 1234, + slug: 'microsoft/vscode', + title: "Fix memory leak in extension host (why is this so hard! It's been months!)", + type: 'GH_PR_ADD_COMMENT', + unique_key: '1', + } satisfies GitHubPRAddCommentSpot, + }, + { + isOpenTab: false, + isSent: false, + isTrashed: false, + latestDraft: { + content: + "I've been using GitLens for years and it's absolutely essential for my workflow. The inline blame annotations are incredibly helpful when...", + stats: { + charCount: 180, + codeBlocks: [], + images: [], + links: [ + { + text: 'GitLens', + url: 'https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens', + }, + ], + }, + time: Date.now() - 1000 * 60 * 60 * 2, + }, + spot: { + subreddit: 'programming', + title: "Re: What's your favorite VS Code extension?", + type: 'REDDIT', + unique_key: '2', + } satisfies RedditSpot, + }, + { + isOpenTab: true, + isSent: false, + isTrashed: false, + latestDraft: { + content: + "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", + stats: { + charCount: 456, + codeBlocks: [{ code: 'useEffect(() => { /* async code */ }, [])', language: 'javascript' }], + images: [], + links: [], + }, + time: Date.now() - 1000 * 60 * 60 * 5, + }, + spot: { + domain: 'github.com', + number: 5678, + slug: 'facebook/react', + title: 'Unexpected behavior with useEffect cleanup', + type: 'GH_ISSUE_ADD_COMMENT', + unique_key: '3', + } satisfies GitHubIssueAddCommentSpot, + }, + { + isOpenTab: false, + isSent: true, + isTrashed: false, + latestDraft: { + content: + 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', + stats: { + charCount: 322, + codeBlocks: [], + images: [ + { url: 'routing-diagram.png' }, + { url: 'example-1.png' }, + { url: 'example-2.png' }, + { url: 'architecture.png' }, + ], + links: [ + { text: 'Routing docs', url: 'https://nextjs.org/docs/routing' }, + { text: 'Examples', url: 'https://github.com/vercel/next.js/tree/main/examples' }, + { + text: 'Migration guide', + url: 'https://nextjs.org/docs/app/building-your-application/upgrading', + }, + ], + }, + time: Date.now() - 1000 * 60 * 60 * 24, + }, + spot: { + domain: 'github', + number: 9012, + slug: 'vercel/next.js', + title: 'Update routing documentation', + type: 'GH_PR_ADD_COMMENT', + unique_key: '4', + } satisfies GitHubPRAddCommentSpot, + }, + { + isOpenTab: true, + isSent: false, + isTrashed: true, + latestDraft: { + content: + 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', + stats: { + charCount: 678, + codeBlocks: [ + { code: 'import { Worker } from "worker_threads"', language: 'javascript' }, + { code: 'new Worker("./worker.mjs", { type: "module" })', language: 'javascript' }, + { code: 'import { parentPort } from "worker_threads"', language: 'javascript' }, + { code: 'interface WorkerOptions { type: "module" }', language: 'typescript' }, + { code: 'await import("./dynamic-module.mjs")', language: 'javascript' }, + { code: 'export default function workerTask() {}', language: 'javascript' }, + { code: 'const result = await workerPromise', language: 'javascript' }, + ], + images: [{ alt: 'ESM Worker Architecture', url: 'worker-architecture.png' }], + links: [ + { + text: 'TSC Meeting Notes', + url: 'https://github.com/nodejs/TSC/blob/main/meetings/2023-11-01.md', + }, + { text: 'ESM Spec', url: 'https://tc39.es/ecma262/' }, + { text: 'Worker Threads docs', url: 'https://nodejs.org/api/worker_threads.html' }, + { text: 'Implementation guide', url: 'https://nodejs.org/api/esm.html' }, + { text: 'Related issue', url: 'https://github.com/nodejs/node/issues/30682' }, + ], + }, + time: Date.now() - 1000 * 60 * 60 * 48, + }, + spot: { + domain: 'github.com', + number: 3456, + slug: 'nodejs/node', + title: 'Add support for ESM in worker threads', + type: 'GH_PR_ADD_COMMENT', + unique_key: '5', + } satisfies GitHubPRAddCommentSpot, + }, +] From eb527aa1df52d907d1ac1deaff882c67bba6a45a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 12 Sep 2025 14:21:06 -0700 Subject: [PATCH 087/109] Massaga data model towards our prototype. --- browser-extension/src/entrypoints/background.ts | 4 ++-- browser-extension/src/lib/enhancer.ts | 7 +------ browser-extension/src/lib/messages.ts | 4 ++-- browser-extension/tests/playground/replica.tsx | 9 +-------- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/browser-extension/src/entrypoints/background.ts b/browser-extension/src/entrypoints/background.ts index 3dcdf7d..8da2cb0 100644 --- a/browser-extension/src/entrypoints/background.ts +++ b/browser-extension/src/entrypoints/background.ts @@ -1,4 +1,4 @@ -import type { CommentDraft, CommentEvent, CommentSpot } from '@/lib/enhancer' +import type { CommentEvent, CommentSpot } from '@/lib/enhancer' import type { GetOpenSpotsResponse, ToBackgroundMessage } from '@/lib/messages' import { CLOSE_MESSAGE_PORT, @@ -15,7 +15,7 @@ export interface Tab { export interface CommentState { tab: Tab spot: CommentSpot - drafts: [number, CommentDraft][] + drafts: [number, string][] } export const openSpots = new Map() diff --git a/browser-extension/src/lib/enhancer.ts b/browser-extension/src/lib/enhancer.ts index 06b294f..585c98e 100644 --- a/browser-extension/src/lib/enhancer.ts +++ b/browser-extension/src/lib/enhancer.ts @@ -11,17 +11,12 @@ export interface CommentSpot { type: string } -export interface CommentDraft { - title?: string - body: string -} - export type CommentEventType = 'ENHANCED' | 'LOST_FOCUS' | 'DESTROYED' export interface CommentEvent { type: CommentEventType spot: CommentSpot - draft?: CommentDraft + draft?: string } /** Wraps the textareas of a given platform with Gitcasso's enhancements. */ diff --git a/browser-extension/src/lib/messages.ts b/browser-extension/src/lib/messages.ts index e04e8c5..ed21222 100644 --- a/browser-extension/src/lib/messages.ts +++ b/browser-extension/src/lib/messages.ts @@ -1,4 +1,4 @@ -import type { CommentDraft, CommentEvent, CommentSpot } from './enhancer' +import type { CommentEvent, CommentSpot } from './enhancer' // Message handler response types export const CLOSE_MESSAGE_PORT = false as const // No response will be sent @@ -31,7 +31,7 @@ export interface GetOpenSpotsResponse { windowId: number } spot: CommentSpot - drafts: Array<[number, CommentDraft]> + drafts: Array<[number, string]> }> } diff --git a/browser-extension/tests/playground/replica.tsx b/browser-extension/tests/playground/replica.tsx index ba8f68e..37e013f 100644 --- a/browser-extension/tests/playground/replica.tsx +++ b/browser-extension/tests/playground/replica.tsx @@ -25,14 +25,7 @@ const gh_issue: GitHubIssueAddCommentSpot = { const spots: CommentSpot[] = [gh_pr, gh_issue] const sampleSpots: CommentState[] = spots.map((spot) => { const state: CommentState = { - drafts: [ - [ - 0, - { - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, - ], - ], + drafts: [[0, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.']], spot, tab: { tabId: 123, From dcfa6faf266add02f3a36c643aa6b17d7d9a0ab1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 15 Sep 2025 22:21:23 -0700 Subject: [PATCH 088/109] Adapt the new github types to our domain. --- ...IssueNewComment.ts => githubIssueNewComment.tsx} | 13 +++++++------ .../src/lib/enhancers/github/githubPRAddComment.tsx | 4 ++-- ...githubPRNewComment.ts => githubPRNewComment.tsx} | 13 +++++++------ 3 files changed, 16 insertions(+), 14 deletions(-) rename browser-extension/src/lib/enhancers/github/{githubIssueNewComment.ts => githubIssueNewComment.tsx} (90%) rename browser-extension/src/lib/enhancers/github/{githubPRNewComment.ts => githubPRNewComment.tsx} (91%) diff --git a/browser-extension/src/lib/enhancers/github/githubIssueNewComment.ts b/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx similarity index 90% rename from browser-extension/src/lib/enhancers/github/githubIssueNewComment.ts rename to browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx index 01c0333..4953cc8 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueNewComment.ts +++ b/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx @@ -52,13 +52,14 @@ export class GitHubIssueNewCommentEnhancer implements CommentEnhancer + New Issue + {slug} + + ) } buildUrl(spot: GitHubIssueNewCommentSpot): string { diff --git a/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx index b75e4b4..2d730e8 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx @@ -65,10 +65,10 @@ export class GitHubPRAddCommentEnhancer implements CommentEnhancer + <> {slug} PR #{number} - + ) } } diff --git a/browser-extension/src/lib/enhancers/github/githubPRNewComment.ts b/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx similarity index 91% rename from browser-extension/src/lib/enhancers/github/githubPRNewComment.ts rename to browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx index 79f217b..ebdb720 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRNewComment.ts +++ b/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx @@ -57,13 +57,14 @@ export class GitHubPRNewCommentEnhancer implements CommentEnhancer + New PR + {slug} + + ) } buildUrl(spot: GitHubPRNewCommentSpot): string { From a39d3f931323216468ffce8bf370524b851bac0d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 15 Sep 2025 22:21:35 -0700 Subject: [PATCH 089/109] Move interfaces around. --- .../src/entrypoints/background.ts | 14 ++++++ browser-extension/tests/playground/claude.tsx | 44 ++++++++++++++----- .../tests/playground/replicaData.tsx | 24 ++-------- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/browser-extension/src/entrypoints/background.ts b/browser-extension/src/entrypoints/background.ts index 8da2cb0..577e21c 100644 --- a/browser-extension/src/entrypoints/background.ts +++ b/browser-extension/src/entrypoints/background.ts @@ -1,4 +1,5 @@ import type { CommentEvent, CommentSpot } from '@/lib/enhancer' +import type { DraftStats } from '@/lib/enhancers/draftStats' import type { GetOpenSpotsResponse, ToBackgroundMessage } from '@/lib/messages' import { CLOSE_MESSAGE_PORT, @@ -17,6 +18,18 @@ export interface CommentState { spot: CommentSpot drafts: [number, string][] } +interface Draft { + content: string + time: number + stats: DraftStats +} +export interface CommentTableRow { + spot: CommentSpot, + latestDraft: Draft + isOpenTab: boolean + isSent: boolean + isTrashed: boolean +} export const openSpots = new Map() @@ -52,6 +65,7 @@ export function handlePopupMessage( ): typeof CLOSE_MESSAGE_PORT | typeof KEEP_PORT_OPEN { if (isGetOpenSpotsMessage(message)) { const spots: CommentState[] = Array.from(openSpots.values()) + const response: GetOpenSpotsResponse = { spots } sendResponse(response) return KEEP_PORT_OPEN diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 2bd93da..547b346 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -18,11 +18,12 @@ import { import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' import { - type CommentTableRow, generateMockDrafts, - isGitHubDraft, - isRedditDraft, + RedditSpot } from './replicaData' +import type { CommentTableRow } from '@/entrypoints/background' +import { CommentSpot } from '@/lib/enhancer' +import { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' interface FilterState { sentFilter: 'both' | 'sent' | 'unsent' @@ -151,7 +152,7 @@ const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps< } // Helper function for relative time -const timeAgo = (date: Date | number) => { +function timeAgo(date: Date | number): string { const timestamp = typeof date === 'number' ? date : date.getTime() const seconds = Math.floor((Date.now() - timestamp) / 1000) const intervals = [ @@ -170,6 +171,26 @@ const timeAgo = (date: Date | number) => { return 'just now' } +/** Returns all leaf values of an arbitrary object as strings. */ +function* allLeafValues(obj: any, visited = new Set()): Generator { + if (visited.has(obj) || obj == null) return + if (typeof obj === 'string') yield obj + else if (typeof obj === 'number') yield String(obj) + else if (typeof obj === 'object') { + visited.add(obj) + for (const key in obj) { + yield* allLeafValues(obj[key], visited) + } + } +} + +function isGitHubDraft(spot: CommentSpot): spot is GitHubIssueAddCommentSpot { + return spot.type === 'GH_PR_ADD_COMMENT' || spot.type === 'GH_ISSUE_ADD_COMMENT' +} +function isRedditDraft(spot: CommentSpot): spot is RedditSpot { + return spot.type === 'REDDIT' +} + export const ClaudePrototype = () => { const [drafts] = useState(generateMockDrafts()) const [selectedIds, setSelectedIds] = useState(new Set()) @@ -193,11 +214,14 @@ export const ClaudePrototype = () => { } if (filters.searchQuery) { const query = filters.searchQuery.toLowerCase() - filtered = filtered.filter((d) => - [d.spot.title, d.latestDraft.content, (d.spot as any).slug, (d.spot as any).subreddit].some( - (value) => value && String(value).toLowerCase().includes(query), - ), - ) + filtered = filtered.filter((d) => { + for (const value of allLeafValues(d)) { + if (value.toLowerCase().includes(query)) { + return true // Early exit on first match + } + } + return false + }) } // sort by newest filtered.sort((a, b) => b.latestDraft.time - a.latestDraft.time) @@ -442,7 +466,7 @@ function commentRow( {/* Title */}
    - {row.spot.title} + TODO_title {row.isTrashed && } diff --git a/browser-extension/tests/playground/replicaData.tsx b/browser-extension/tests/playground/replicaData.tsx index d4fa676..41f0ad5 100644 --- a/browser-extension/tests/playground/replicaData.tsx +++ b/browser-extension/tests/playground/replicaData.tsx @@ -1,32 +1,14 @@ import type { CommentSpot } from '@/lib/enhancer' -import type { DraftStats } from '@/lib/enhancers/draftStats' +import type { CommentTableRow } from '@/entrypoints/background' import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' -interface RedditSpot extends CommentSpot { +export interface RedditSpot extends CommentSpot { title: string subreddit: string type: 'REDDIT' } -interface Draft { - content: string - time: number - stats: DraftStats -} -export interface CommentTableRow { - spot: GitHubOrReddit - latestDraft: Draft - isOpenTab: boolean - isSent: boolean - isTrashed: boolean -} -type GitHubOrReddit = GitHubIssueAddCommentSpot | GitHubPRAddCommentSpot | RedditSpot -export const isGitHubDraft = (spot: GitHubOrReddit): spot is GitHubIssueAddCommentSpot => { - return spot.type === 'GH_PR_ADD_COMMENT' || spot.type === 'GH_ISSUE_ADD_COMMENT' -} -export const isRedditDraft = (spot: GitHubOrReddit): spot is RedditSpot => { - return spot.type === 'REDDIT' -} + export const generateMockDrafts = (): CommentTableRow[] => [ { isOpenTab: true, From 79a7d51855ca2e6418b12fb3d323e872d69fd763 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 15 Sep 2025 22:29:23 -0700 Subject: [PATCH 090/109] It works. --- .../tests/playground/replicaData.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/browser-extension/tests/playground/replicaData.tsx b/browser-extension/tests/playground/replicaData.tsx index 41f0ad5..9f8dcde 100644 --- a/browser-extension/tests/playground/replicaData.tsx +++ b/browser-extension/tests/playground/replicaData.tsx @@ -9,6 +9,8 @@ export interface RedditSpot extends CommentSpot { type: 'REDDIT' } +const withSpot = (spot: T): T => spot + export const generateMockDrafts = (): CommentTableRow[] => [ { isOpenTab: true, @@ -35,14 +37,14 @@ export const generateMockDrafts = (): CommentTableRow[] => [ }, time: Date.now() - 1000 * 60 * 30, }, - spot: { + spot: withSpot({ domain: 'github.com', number: 1234, slug: 'microsoft/vscode', title: "Fix memory leak in extension host (why is this so hard! It's been months!)", type: 'GH_PR_ADD_COMMENT', unique_key: '1', - } satisfies GitHubPRAddCommentSpot, + } satisfies GitHubPRAddCommentSpot), }, { isOpenTab: false, @@ -64,12 +66,12 @@ export const generateMockDrafts = (): CommentTableRow[] => [ }, time: Date.now() - 1000 * 60 * 60 * 2, }, - spot: { + spot: withSpot({ subreddit: 'programming', title: "Re: What's your favorite VS Code extension?", type: 'REDDIT', unique_key: '2', - } satisfies RedditSpot, + } satisfies RedditSpot), }, { isOpenTab: true, @@ -86,14 +88,14 @@ export const generateMockDrafts = (): CommentTableRow[] => [ }, time: Date.now() - 1000 * 60 * 60 * 5, }, - spot: { + spot: withSpot({ domain: 'github.com', number: 5678, slug: 'facebook/react', title: 'Unexpected behavior with useEffect cleanup', type: 'GH_ISSUE_ADD_COMMENT', unique_key: '3', - } satisfies GitHubIssueAddCommentSpot, + } satisfies GitHubIssueAddCommentSpot), }, { isOpenTab: false, @@ -122,14 +124,14 @@ export const generateMockDrafts = (): CommentTableRow[] => [ }, time: Date.now() - 1000 * 60 * 60 * 24, }, - spot: { + spot: withSpot({ domain: 'github', number: 9012, slug: 'vercel/next.js', title: 'Update routing documentation', type: 'GH_PR_ADD_COMMENT', unique_key: '4', - } satisfies GitHubPRAddCommentSpot, + } satisfies GitHubPRAddCommentSpot), }, { isOpenTab: true, @@ -163,13 +165,13 @@ export const generateMockDrafts = (): CommentTableRow[] => [ }, time: Date.now() - 1000 * 60 * 60 * 48, }, - spot: { + spot: withSpot({ domain: 'github.com', number: 3456, slug: 'nodejs/node', title: 'Add support for ESM in worker threads', type: 'GH_PR_ADD_COMMENT', unique_key: '5', - } satisfies GitHubPRAddCommentSpot, + } satisfies GitHubPRAddCommentSpot), }, ] From 2393a37e0c29de4649e4aad2bd84cebab35cb53a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 15 Sep 2025 22:35:28 -0700 Subject: [PATCH 091/109] fixup snapshots --- browser-extension/src/entrypoints/background.ts | 2 +- .../src/lib/enhancers/github/githubIssueNewComment.tsx | 2 +- .../src/lib/enhancers/github/githubPRNewComment.tsx | 2 +- browser-extension/tests/lib/enhancers/github.test.ts | 4 ++-- browser-extension/tests/playground/claude.tsx | 9 +++------ browser-extension/tests/playground/replicaData.tsx | 2 +- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/browser-extension/src/entrypoints/background.ts b/browser-extension/src/entrypoints/background.ts index 577e21c..7366949 100644 --- a/browser-extension/src/entrypoints/background.ts +++ b/browser-extension/src/entrypoints/background.ts @@ -24,7 +24,7 @@ interface Draft { stats: DraftStats } export interface CommentTableRow { - spot: CommentSpot, + spot: CommentSpot latestDraft: Draft isOpenTab: boolean isSent: boolean diff --git a/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx b/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx index 4953cc8..db00114 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx @@ -57,7 +57,7 @@ export class GitHubIssueNewCommentEnhancer implements CommentEnhancer New Issue - {slug} + {slug} ) } diff --git a/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx b/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx index ebdb720..cd995e3 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx @@ -62,7 +62,7 @@ export class GitHubPRNewCommentEnhancer implements CommentEnhancer New PR - {slug} + {slug} ) } diff --git a/browser-extension/tests/lib/enhancers/github.test.ts b/browser-extension/tests/lib/enhancers/github.test.ts index 7242ec6..df365a6 100644 --- a/browser-extension/tests/lib/enhancers/github.test.ts +++ b/browser-extension/tests/lib/enhancers/github.test.ts @@ -23,7 +23,7 @@ describe('github', () => { } `) expect(enhancedTextarea?.enhancer.tableRow(enhancedTextarea.spot)).toMatchInlineSnapshot(` - + @@ -35,7 +35,7 @@ describe('github', () => { PR # 517 - + `) }) usingHar('gh_new_pr').it('should create the correct spot object', async () => { diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 547b346..1699783 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -17,13 +17,10 @@ import { } from 'lucide-react' import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' -import { - generateMockDrafts, - RedditSpot -} from './replicaData' import type { CommentTableRow } from '@/entrypoints/background' -import { CommentSpot } from '@/lib/enhancer' -import { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' +import type { CommentSpot } from '@/lib/enhancer' +import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' +import { generateMockDrafts, type RedditSpot } from './replicaData' interface FilterState { sentFilter: 'both' | 'sent' | 'unsent' diff --git a/browser-extension/tests/playground/replicaData.tsx b/browser-extension/tests/playground/replicaData.tsx index 9f8dcde..7ee7f5f 100644 --- a/browser-extension/tests/playground/replicaData.tsx +++ b/browser-extension/tests/playground/replicaData.tsx @@ -1,5 +1,5 @@ -import type { CommentSpot } from '@/lib/enhancer' import type { CommentTableRow } from '@/entrypoints/background' +import type { CommentSpot } from '@/lib/enhancer' import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' From 0f0aa4f3ef529a3e498af81cdac4659e7f3c8a27 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 09:29:53 -0700 Subject: [PATCH 092/109] Refactor more playground stuff into the real enhancers. --- browser-extension/src/components/SpotRow.tsx | 4 +- browser-extension/src/lib/enhancer.ts | 4 +- .../github/githubIssueAddComment.tsx | 21 ++++++--- .../github/githubIssueNewComment.tsx | 6 ++- .../enhancers/github/githubPRAddComment.tsx | 6 ++- .../enhancers/github/githubPRNewComment.tsx | 6 ++- .../tests/lib/enhancers/github.test.ts | 30 ++++++++----- browser-extension/tests/playground/claude.tsx | 43 +++---------------- 8 files changed, 61 insertions(+), 59 deletions(-) diff --git a/browser-extension/src/components/SpotRow.tsx b/browser-extension/src/components/SpotRow.tsx index a394901..1c88df5 100644 --- a/browser-extension/src/components/SpotRow.tsx +++ b/browser-extension/src/components/SpotRow.tsx @@ -34,7 +34,9 @@ export function SpotRow({ return ( - {enhancer.tableRow(commentState.spot)} + + {enhancer.tableUpperDecoration(commentState.spot)} + ) } diff --git a/browser-extension/src/lib/enhancer.ts b/browser-extension/src/lib/enhancer.ts index 585c98e..5c9e95b 100644 --- a/browser-extension/src/lib/enhancer.ts +++ b/browser-extension/src/lib/enhancer.ts @@ -37,5 +37,7 @@ export interface CommentEnhancer { */ enhance(textarea: HTMLTextAreaElement, spot: Spot): OverTypeInstance /** Returns a ReactNode which will be displayed in the table row. */ - tableRow(spot: Spot): ReactNode + tableUpperDecoration(spot: Spot): ReactNode + /** The default title of a row */ + tableTitle(spot: Spot): string } diff --git a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx index 7c75ee8..9faa633 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubIssueAddComment.tsx @@ -1,3 +1,4 @@ +import { IssueOpenedIcon } from '@primer/octicons-react' import OverType, { type OverTypeInstance } from 'overtype' import type React from 'react' import type { CommentEnhancer, CommentSpot } from '@/lib/enhancer' @@ -58,13 +59,21 @@ export class GitHubIssueAddCommentEnhancer implements CommentEnhancer - {slug} - Issue #{number} - + <> + + + + #{spot.number} + + {spot.slug} + + ) } + + tableTitle(_spot: GitHubIssueAddCommentSpot): string { + return 'TITLE_TODO' + } } diff --git a/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx b/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx index db00114..3e8ae53 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx @@ -52,7 +52,7 @@ export class GitHubIssueNewCommentEnhancer implements CommentEnhancer @@ -62,6 +62,10 @@ export class GitHubIssueNewCommentEnhancer implements CommentEnhancer @@ -71,4 +71,8 @@ export class GitHubPRAddCommentEnhancer implements CommentEnhancer ) } + + tableTitle(_spot: GitHubPRAddCommentSpot): string { + return 'TITLE_TODO' + } } diff --git a/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx b/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx index cd995e3..a654f48 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx +++ b/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx @@ -57,7 +57,7 @@ export class GitHubPRNewCommentEnhancer implements CommentEnhancer @@ -67,6 +67,10 @@ export class GitHubPRNewCommentEnhancer implements CommentEnhancer { "unique_key": "github.com:diffplug/selfie:517", } `) - expect(enhancedTextarea?.enhancer.tableRow(enhancedTextarea.spot)).toMatchInlineSnapshot(` + expect( + enhancedTextarea?.enhancer.tableUpperDecoration(enhancedTextarea.spot), + ).toMatchInlineSnapshot(` { } `) // Test the tableRow method - expect(enhancedTextarea?.enhancer.tableRow(enhancedTextarea.spot)).toMatchInlineSnapshot(` - + expect( + enhancedTextarea?.enhancer.tableUpperDecoration(enhancedTextarea.spot), + ).toMatchInlineSnapshot(` + - diffplug/selfie + - - Issue # - 523 - - + diffplug/selfie + + `) }) usingHar('gh_new_issue').it('should create the correct spot object', async () => { diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index 1699783..a490e7a 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,4 +1,3 @@ -import { GitPullRequestIcon, IssueOpenedIcon } from '@primer/octicons-react' import { cva, type VariantProps } from 'class-variance-authority' import { Clock, @@ -18,9 +17,8 @@ import { import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' import type { CommentTableRow } from '@/entrypoints/background' -import type { CommentSpot } from '@/lib/enhancer' -import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' -import { generateMockDrafts, type RedditSpot } from './replicaData' +import { EnhancerRegistry } from '@/lib/registries' +import { generateMockDrafts } from './replicaData' interface FilterState { sentFilter: 'both' | 'sent' | 'unsent' @@ -181,13 +179,6 @@ function* allLeafValues(obj: any, visited = new Set()): Generator { } } -function isGitHubDraft(spot: CommentSpot): spot is GitHubIssueAddCommentSpot { - return spot.type === 'GH_PR_ADD_COMMENT' || spot.type === 'GH_ISSUE_ADD_COMMENT' -} -function isRedditDraft(spot: CommentSpot): spot is RedditSpot { - return spot.type === 'REDDIT' -} - export const ClaudePrototype = () => { const [drafts] = useState(generateMockDrafts()) const [selectedIds, setSelectedIds] = useState(new Set()) @@ -396,6 +387,7 @@ export const ClaudePrototype = () => { ) } +const enhancers = new EnhancerRegistry() function commentRow( row: CommentTableRow, selectedIds: Set, @@ -403,6 +395,7 @@ function commentRow( _handleOpen: (url: string) => void, _handleTrash: (row: CommentTableRow) => void, ) { + const enhancer = enhancers.enhancerFor(row.spot) return ( @@ -418,31 +411,7 @@ function commentRow( {/* Context line */}
    - - {row.spot.type === 'GH_PR_ADD_COMMENT' && } - {row.spot.type === 'GH_ISSUE_ADD_COMMENT' && } - {row.spot.type === 'REDDIT' && ( - Reddit - )} - - - {isGitHubDraft(row.spot) && ( - <> - #{row.spot.number} - - {row.spot.slug} - - - )} - {isRedditDraft(row.spot) && ( - - r/{row.spot.subreddit} - - )} + {enhancer.tableUpperDecoration(row.spot)}
    {row.latestDraft.stats.links.length > 0 && ( @@ -463,7 +432,7 @@ function commentRow( {/* Title */}
    - TODO_title + {enhancer.tableTitle(row.spot)} {row.isTrashed && } From 0fbd95de14aa1751638da77916a6a6b03dab1c8d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 10:52:54 -0700 Subject: [PATCH 093/109] Add an enhancer to function for unknown types. --- .../lib/enhancers/CommentEnhancerMissing.tsx | 52 +++++++++++++++++++ browser-extension/src/lib/registries.ts | 3 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx diff --git a/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx b/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx new file mode 100644 index 0000000..7c01a35 --- /dev/null +++ b/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx @@ -0,0 +1,52 @@ +import type { OverTypeInstance } from 'overtype' +import type { ReactNode } from 'react' +import type { CommentEnhancer, CommentSpot } from '../enhancer' + +/** Used when an entry is in the table which we don't recognize. */ +export class CommentEnhancerMissing implements CommentEnhancer { + tableUpperDecoration(spot: CommentSpot): ReactNode { + return ( + + ) + } + tableTitle(spot: CommentSpot): string { + return `Unknown type '${spot.type}'` + } + forSpotTypes(): string[] { + throw new Error('Method not implemented.') + } + tryToEnhance(_textarea: HTMLTextAreaElement): CommentSpot | null { + throw new Error('Method not implemented.') + } + prepareForFirstEnhancement(): void { + throw new Error('Method not implemented.') + } + enhance(_textarea: HTMLTextAreaElement, _spot: CommentSpot): OverTypeInstance { + throw new Error('Method not implemented.') + } +} diff --git a/browser-extension/src/lib/registries.ts b/browser-extension/src/lib/registries.ts index d6cf1aa..2f53313 100644 --- a/browser-extension/src/lib/registries.ts +++ b/browser-extension/src/lib/registries.ts @@ -1,6 +1,7 @@ import type { OverTypeInstance } from 'overtype' import OverType from 'overtype' import type { CommentEnhancer, CommentSpot } from './enhancer' +import { CommentEnhancerMissing } from './enhancers/CommentEnhancerMissing' import { GitHubIssueAddCommentEnhancer } from './enhancers/github/githubIssueAddComment' import { GitHubIssueNewCommentEnhancer } from './enhancers/github/githubIssueNewComment' import { GitHubPRAddCommentEnhancer } from './enhancers/github/githubPRAddComment' @@ -55,7 +56,7 @@ export class EnhancerRegistry { } enhancerFor(spot: T): CommentEnhancer { - return this.byType.get(spot.type)! as CommentEnhancer + return (this.byType.get(spot.type) || new CommentEnhancerMissing()) as CommentEnhancer } tryToEnhance(textarea: HTMLTextAreaElement): EnhancedTextarea | null { From 438b37304cc69c863b5cfaa14539f78b8849de81 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 12:11:06 -0700 Subject: [PATCH 094/109] Navigate design out into its own thing. --- browser-extension/src/components/design.tsx | 64 +++++++++++++++ .../src/entrypoints/popup/popup.tsx | 6 ++ browser-extension/tests/playground/claude.tsx | 78 ++----------------- 3 files changed, 75 insertions(+), 73 deletions(-) create mode 100644 browser-extension/src/components/design.tsx diff --git a/browser-extension/src/components/design.tsx b/browser-extension/src/components/design.tsx new file mode 100644 index 0000000..998a5a2 --- /dev/null +++ b/browser-extension/src/components/design.tsx @@ -0,0 +1,64 @@ +import { cva } from 'class-variance-authority' +import { + Clock, + Code, + EyeOff, + Image, + Link, + MailCheck, + MessageSquareDashed, + Monitor, + Settings, + TextSelect, + Trash2, +} from 'lucide-react' + +// CVA configuration for stat badges +export const statBadge = cva( + 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal h-5', + { + defaultVariants: { + clickable: false, + }, + variants: { + clickable: { + false: '', + true: 'cursor-pointer border border-transparent hover:border-current border-dashed', + }, + selected: { + false: '', + true: '!border-solid !border-current', + }, + type: { + blank: 'bg-transparent text-gray-700', + code: 'bg-pink-50 text-pink-700', + hideTrashed: 'bg-transparent text-gray-700', + image: 'bg-purple-50 text-purple-700', + link: 'bg-blue-50 text-blue-700', + open: 'bg-cyan-50 text-cyan-700', + sent: 'bg-green-50 text-green-700', + settings: 'bg-gray-50 text-gray-700', + text: 'bg-gray-50 text-gray-700', + time: 'bg-gray-50 text-gray-700', + trashed: 'bg-gray-50 text-yellow-700', + unsent: 'bg-amber-100 text-amber-700', + }, + }, + }, +) + +// Map types to their icons +export const typeIcons = { + blank: Code, + code: Code, + hideTrashed: EyeOff, + image: Image, + link: Link, + open: Monitor, + sent: MailCheck, + settings: Settings, + text: TextSelect, + time: Clock, + trashed: Trash2, + unsent: MessageSquareDashed, +} as const diff --git a/browser-extension/src/entrypoints/popup/popup.tsx b/browser-extension/src/entrypoints/popup/popup.tsx index fa1cea2..6b53393 100644 --- a/browser-extension/src/entrypoints/popup/popup.tsx +++ b/browser-extension/src/entrypoints/popup/popup.tsx @@ -32,6 +32,12 @@ function switchToTab(tabId: number, windowId: number): void { const enhancers = new EnhancerRegistry() +export interface FilterState { + sentFilter: 'both' | 'sent' | 'unsent' + searchQuery: string + showTrashed: boolean +} + function PopupApp() { const [spots, setSpots] = React.useState([]) const [isLoading, setIsLoading] = React.useState(true) diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index a490e7a..b186093 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,83 +1,15 @@ -import { cva, type VariantProps } from 'class-variance-authority' -import { - Clock, - Code, - Eye, - EyeOff, - Image, - Link, - MailCheck, - MessageSquareDashed, - Monitor, - Search, - Settings, - TextSelect, - Trash2, -} from 'lucide-react' +import type { VariantProps } from 'class-variance-authority' +import { Eye, EyeOff, Search, Settings, Trash2 } from 'lucide-react' import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' +import { statBadge, typeIcons } from '@/components/design' import type { CommentTableRow } from '@/entrypoints/background' +import type { FilterState } from '@/entrypoints/popup/popup' import { EnhancerRegistry } from '@/lib/registries' import { generateMockDrafts } from './replicaData' -interface FilterState { - sentFilter: 'both' | 'sent' | 'unsent' - searchQuery: string - showTrashed: boolean -} - -// CVA configuration for stat badges -const statBadge = cva( - 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal h-5', - { - defaultVariants: { - clickable: false, - }, - variants: { - clickable: { - false: '', - true: 'cursor-pointer border border-transparent hover:border-current border-dashed', - }, - selected: { - false: '', - true: '!border-solid !border-current', - }, - type: { - blank: 'bg-transparent text-gray-700', - code: 'bg-pink-50 text-pink-700', - hideTrashed: 'bg-transparent text-gray-700', - image: 'bg-purple-50 text-purple-700', - link: 'bg-blue-50 text-blue-700', - open: 'bg-cyan-50 text-cyan-700', - sent: 'bg-green-50 text-green-700', - settings: 'bg-gray-50 text-gray-700', - text: 'bg-gray-50 text-gray-700', - time: 'bg-gray-50 text-gray-700', - trashed: 'bg-gray-50 text-yellow-700', - unsent: 'bg-amber-100 text-amber-700', - }, - }, - }, -) - -// Map types to their icons -const typeIcons = { - blank: Code, - code: Code, - hideTrashed: EyeOff, - image: Image, - link: Link, - open: Monitor, - sent: MailCheck, - settings: Settings, - text: TextSelect, - time: Clock, - trashed: Trash2, - unsent: MessageSquareDashed, -} as const - // StatBadge component -type BadgeProps = VariantProps & { +export type BadgeProps = VariantProps & { type: keyof typeof typeIcons text?: number | string } From 9f4bb04e93e76d8269be685f9628b64dcfc7f9b7 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 12:14:35 -0700 Subject: [PATCH 095/109] Factor `Badge` out into its own thing. --- browser-extension/src/components/Badge.tsx | 26 +++++++++++++++++++ browser-extension/tests/playground/claude.tsx | 24 +---------------- 2 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 browser-extension/src/components/Badge.tsx diff --git a/browser-extension/src/components/Badge.tsx b/browser-extension/src/components/Badge.tsx new file mode 100644 index 0000000..0a4ad0c --- /dev/null +++ b/browser-extension/src/components/Badge.tsx @@ -0,0 +1,26 @@ +import type { VariantProps } from 'class-variance-authority' +import { twMerge } from 'tailwind-merge' +import { statBadge, typeIcons } from '@/components/design' + +export type BadgeProps = VariantProps & { + type: keyof typeof typeIcons + text?: number | string +} + +const Badge = ({ text, type }: BadgeProps) => { + const Icon = typeIcons[type] + return ( + + {type === 'blank' || } + {text || type} + + ) +} + +export default Badge diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index b186093..2a496ed 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -1,35 +1,13 @@ -import type { VariantProps } from 'class-variance-authority' import { Eye, EyeOff, Search, Settings, Trash2 } from 'lucide-react' import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' +import Badge from '@/components/Badge' import { statBadge, typeIcons } from '@/components/design' import type { CommentTableRow } from '@/entrypoints/background' import type { FilterState } from '@/entrypoints/popup/popup' import { EnhancerRegistry } from '@/lib/registries' import { generateMockDrafts } from './replicaData' -// StatBadge component -export type BadgeProps = VariantProps & { - type: keyof typeof typeIcons - text?: number | string -} - -const Badge = ({ text, type }: BadgeProps) => { - const Icon = typeIcons[type] - return ( - - {type === 'blank' || } - {text || type} - - ) -} - interface Segment { text?: string type: keyof typeof typeIcons From 1d3d3dfa5b13025546f39173d25c2cb65e3bf6e7 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 12:15:36 -0700 Subject: [PATCH 096/109] Rename `statBadge` to `badgeCVA`. --- browser-extension/src/components/Badge.tsx | 6 +++--- browser-extension/src/components/design.tsx | 2 +- browser-extension/tests/playground/claude.tsx | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/browser-extension/src/components/Badge.tsx b/browser-extension/src/components/Badge.tsx index 0a4ad0c..68dc2bf 100644 --- a/browser-extension/src/components/Badge.tsx +++ b/browser-extension/src/components/Badge.tsx @@ -1,8 +1,8 @@ import type { VariantProps } from 'class-variance-authority' import { twMerge } from 'tailwind-merge' -import { statBadge, typeIcons } from '@/components/design' +import { badgeCVA, typeIcons } from '@/components/design' -export type BadgeProps = VariantProps & { +export type BadgeProps = VariantProps & { type: keyof typeof typeIcons text?: number | string } @@ -12,7 +12,7 @@ const Badge = ({ text, type }: BadgeProps) => { return ( ({ segments, value, onValueChange }: MultiSegmentProps< return ( + ) + })} +
    + ) +} + +export default MultiSegment diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index b115d7f..cd71b19 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -2,60 +2,13 @@ import { Eye, EyeOff, Search, Settings, Trash2 } from 'lucide-react' import { useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' import Badge from '@/components/Badge' -import { badgeCVA, typeIcons } from '@/components/design' +import { badgeCVA } from '@/components/design' +import MultiSegment from '@/components/MultiSegment' import type { CommentTableRow } from '@/entrypoints/background' import type { FilterState } from '@/entrypoints/popup/popup' import { EnhancerRegistry } from '@/lib/registries' import { generateMockDrafts } from './replicaData' -interface Segment { - text?: string - type: keyof typeof typeIcons - value: T -} -interface MultiSegmentProps { - segments: Segment[] - value: T - onValueChange: (value: T) => void -} - -const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps) => { - return ( -
    - {segments.map((segment, index) => { - const Icon = typeIcons[segment.type] - const isFirst = index === 0 - const isLast = index === segments.length - 1 - - const roundedClasses = - isFirst && isLast - ? '' - : isFirst - ? '!rounded-r-none' - : isLast - ? '!rounded-l-none' - : '!rounded-none' - - return ( - - ) - })} -
    - ) -} - // Helper function for relative time function timeAgo(date: Date | number): string { const timestamp = typeof date === 'number' ? date : date.getTime() From 937a564ab9d418a16199e97615e3be2373a19121 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 12:22:41 -0700 Subject: [PATCH 098/109] Refactor some helper UI functions into `misc.ts`. --- browser-extension/src/components/misc.ts | 31 +++++++++++++++++ browser-extension/tests/playground/claude.tsx | 34 +------------------ 2 files changed, 32 insertions(+), 33 deletions(-) create mode 100644 browser-extension/src/components/misc.ts diff --git a/browser-extension/src/components/misc.ts b/browser-extension/src/components/misc.ts new file mode 100644 index 0000000..baddc6f --- /dev/null +++ b/browser-extension/src/components/misc.ts @@ -0,0 +1,31 @@ +export function timeAgo(date: Date | number): string { + const timestamp = typeof date === 'number' ? date : date.getTime() + const seconds = Math.floor((Date.now() - timestamp) / 1000) + const intervals = [ + { label: 'y', secs: 31536000 }, + { label: 'mo', secs: 2592000 }, + { label: 'w', secs: 604800 }, + { label: 'd', secs: 86400 }, + { label: 'h', secs: 3600 }, + { label: 'm', secs: 60 }, + { label: 's', secs: 1 }, + ] + for (const i of intervals) { + const v = Math.floor(seconds / i.secs) + if (v >= 1) return `${v}${i.label}` + } + return 'just now' +} + +/** Returns all leaf values of an arbitrary object as strings. */ +export function* allLeafValues(obj: any, visited = new Set()): Generator { + if (visited.has(obj) || obj == null) return + if (typeof obj === 'string') yield obj + else if (typeof obj === 'number') yield String(obj) + else if (typeof obj === 'object') { + visited.add(obj) + for (const key in obj) { + yield* allLeafValues(obj[key], visited) + } + } +} diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx index cd71b19..51a9db4 100644 --- a/browser-extension/tests/playground/claude.tsx +++ b/browser-extension/tests/playground/claude.tsx @@ -4,44 +4,12 @@ import { twMerge } from 'tailwind-merge' import Badge from '@/components/Badge' import { badgeCVA } from '@/components/design' import MultiSegment from '@/components/MultiSegment' +import { allLeafValues, timeAgo } from '@/components/misc' import type { CommentTableRow } from '@/entrypoints/background' import type { FilterState } from '@/entrypoints/popup/popup' import { EnhancerRegistry } from '@/lib/registries' import { generateMockDrafts } from './replicaData' -// Helper function for relative time -function timeAgo(date: Date | number): string { - const timestamp = typeof date === 'number' ? date : date.getTime() - const seconds = Math.floor((Date.now() - timestamp) / 1000) - const intervals = [ - { label: 'y', secs: 31536000 }, - { label: 'mo', secs: 2592000 }, - { label: 'w', secs: 604800 }, - { label: 'd', secs: 86400 }, - { label: 'h', secs: 3600 }, - { label: 'm', secs: 60 }, - { label: 's', secs: 1 }, - ] - for (const i of intervals) { - const v = Math.floor(seconds / i.secs) - if (v >= 1) return `${v}${i.label}` - } - return 'just now' -} - -/** Returns all leaf values of an arbitrary object as strings. */ -function* allLeafValues(obj: any, visited = new Set()): Generator { - if (visited.has(obj) || obj == null) return - if (typeof obj === 'string') yield obj - else if (typeof obj === 'number') yield String(obj) - else if (typeof obj === 'object') { - visited.add(obj) - for (const key in obj) { - yield* allLeafValues(obj[key], visited) - } - } -} - export const ClaudePrototype = () => { const [drafts] = useState(generateMockDrafts()) const [selectedIds, setSelectedIds] = useState(new Set()) From 63e9149500631a5531132183c49b55c59746a825 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 13:33:37 -0700 Subject: [PATCH 099/109] Refactor from `CommentState` to `CommentTableRow` --- browser-extension/src/components/SpotRow.tsx | 4 +- .../src/components/SpotTable.tsx | 6 +-- .../src/entrypoints/background.ts | 33 +++++++++--- .../src/entrypoints/popup/popup.tsx | 54 +++++++------------ browser-extension/src/lib/messages.ts | 16 ++---- browser-extension/tests/background.test.ts | 2 + .../tests/playground/replica.tsx | 10 ++-- 7 files changed, 63 insertions(+), 62 deletions(-) diff --git a/browser-extension/src/components/SpotRow.tsx b/browser-extension/src/components/SpotRow.tsx index 1c88df5..fe58847 100644 --- a/browser-extension/src/components/SpotRow.tsx +++ b/browser-extension/src/components/SpotRow.tsx @@ -1,10 +1,10 @@ import { TableCell, TableRow } from '@/components/ui/table' -import type { CommentState } from '@/entrypoints/background' +import type { CommentStorage } from '@/entrypoints/background' import type { EnhancerRegistry } from '@/lib/registries' import { cn } from '@/lib/utils' interface SpotRowProps { - commentState: CommentState + commentState: CommentStorage enhancerRegistry: EnhancerRegistry onClick: () => void className?: string diff --git a/browser-extension/src/components/SpotTable.tsx b/browser-extension/src/components/SpotTable.tsx index 33e26e7..1c02a09 100644 --- a/browser-extension/src/components/SpotTable.tsx +++ b/browser-extension/src/components/SpotTable.tsx @@ -1,12 +1,12 @@ import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import type { CommentState } from '@/entrypoints/background' +import type { CommentStorage } from '@/entrypoints/background' import type { EnhancerRegistry } from '@/lib/registries' import { SpotRow } from './SpotRow' interface SpotTableProps { - spots: CommentState[] + spots: CommentStorage[] enhancerRegistry: EnhancerRegistry - onSpotClick: (spot: CommentState) => void + onSpotClick: (spot: CommentStorage) => void title?: string description?: string headerText?: string diff --git a/browser-extension/src/entrypoints/background.ts b/browser-extension/src/entrypoints/background.ts index 7366949..6a8659b 100644 --- a/browser-extension/src/entrypoints/background.ts +++ b/browser-extension/src/entrypoints/background.ts @@ -1,6 +1,6 @@ import type { CommentEvent, CommentSpot } from '@/lib/enhancer' -import type { DraftStats } from '@/lib/enhancers/draftStats' -import type { GetOpenSpotsResponse, ToBackgroundMessage } from '@/lib/messages' +import { type DraftStats, statsFor } from '@/lib/enhancers/draftStats' +import type { GetTableRowsResponse, ToBackgroundMessage } from '@/lib/messages' import { CLOSE_MESSAGE_PORT, isContentToBackgroundMessage, @@ -13,10 +13,12 @@ export interface Tab { tabId: number windowId: number } -export interface CommentState { +export interface CommentStorage { tab: Tab spot: CommentSpot drafts: [number, string][] + sentOn: number | null + trashedOn: number | null } interface Draft { content: string @@ -31,7 +33,7 @@ export interface CommentTableRow { isTrashed: boolean } -export const openSpots = new Map() +export const openSpots = new Map() export function handleCommentEvent(message: CommentEvent, sender: any): boolean { if ( @@ -40,13 +42,15 @@ export function handleCommentEvent(message: CommentEvent, sender: any): boolean sender.tab?.windowId ) { if (message.type === 'ENHANCED') { - const commentState: CommentState = { + const commentState: CommentStorage = { drafts: [], + sentOn: null, spot: message.spot, tab: { tabId: sender.tab.id, windowId: sender.tab.windowId, }, + trashedOn: null, } openSpots.set(message.spot.unique_key, commentState) } else if (message.type === 'DESTROYED') { @@ -64,9 +68,22 @@ export function handlePopupMessage( sendResponse: (response: any) => void, ): typeof CLOSE_MESSAGE_PORT | typeof KEEP_PORT_OPEN { if (isGetOpenSpotsMessage(message)) { - const spots: CommentState[] = Array.from(openSpots.values()) - - const response: GetOpenSpotsResponse = { spots } + const rows: CommentTableRow[] = Array.from(openSpots.values()).map((storage) => { + const [time, content] = storage.drafts.at(-1)! + const row: CommentTableRow = { + isOpenTab: true, + isSent: storage.sentOn != null, + isTrashed: storage.trashedOn != null, + latestDraft: { + content, + stats: statsFor(content), + time, + }, + spot: storage.spot, + } + return row + }) + const response: GetTableRowsResponse = { rows } sendResponse(response) return KEEP_PORT_OPEN } else if (isSwitchToTabMessage(message)) { diff --git a/browser-extension/src/entrypoints/popup/popup.tsx b/browser-extension/src/entrypoints/popup/popup.tsx index 6b53393..dd55d76 100644 --- a/browser-extension/src/entrypoints/popup/popup.tsx +++ b/browser-extension/src/entrypoints/popup/popup.tsx @@ -1,36 +1,32 @@ import './style.css' import React from 'react' import { createRoot } from 'react-dom/client' -import { SpotTable } from '@/components/SpotTable' -import type { CommentState } from '@/entrypoints/background' +import type { CommentTableRow } from '@/entrypoints/background' import { logger } from '@/lib/logger' -import type { GetOpenSpotsMessage, GetOpenSpotsResponse, SwitchToTabMessage } from '@/lib/messages' -import { EnhancerRegistry } from '@/lib/registries' +import type { GetOpenSpotsMessage, GetTableRowsResponse } from '@/lib/messages' -async function getOpenSpots(): Promise { +async function getOpenSpots(): Promise { logger.debug('Sending message to background script...') try { const message: GetOpenSpotsMessage = { type: 'GET_OPEN_SPOTS' } - const response = (await browser.runtime.sendMessage(message)) as GetOpenSpotsResponse + const response = (await browser.runtime.sendMessage(message)) as GetTableRowsResponse logger.debug('Received response:', response) - return response.spots || [] + return response.rows || [] } catch (error) { logger.error('Error sending message to background:', error) return [] } } -function switchToTab(tabId: number, windowId: number): void { - const message: SwitchToTabMessage = { - tabId, - type: 'SWITCH_TO_TAB', - windowId, - } - browser.runtime.sendMessage(message) - window.close() -} - -const enhancers = new EnhancerRegistry() +// function switchToTab(tabId: number, windowId: number): void { +// const message: SwitchToTabMessage = { +// tabId, +// type: 'SWITCH_TO_TAB', +// windowId, +// } +// browser.runtime.sendMessage(message) +// window.close() +// } export interface FilterState { sentFilter: 'both' | 'sent' | 'unsent' @@ -39,7 +35,7 @@ export interface FilterState { } function PopupApp() { - const [spots, setSpots] = React.useState([]) + const [_spots, setSpots] = React.useState([]) const [isLoading, setIsLoading] = React.useState(true) React.useEffect(() => { @@ -61,26 +57,16 @@ function PopupApp() { return
    Loading...
    } - const handleSpotClick = (spot: CommentState) => { - switchToTab(spot.tab.tabId, spot.tab.windowId) - } + // const handleSpotClick = (spot: CommentTableRow) => { + // console.log('TODO: switchToTab') + // //switchToTab(spot.tab.tabId, spot.tab.windowId) + // } return (

    Open Comment Spots

    -
    - -
    +
    ) } diff --git a/browser-extension/src/lib/messages.ts b/browser-extension/src/lib/messages.ts index ed21222..09d0298 100644 --- a/browser-extension/src/lib/messages.ts +++ b/browser-extension/src/lib/messages.ts @@ -1,4 +1,5 @@ -import type { CommentEvent, CommentSpot } from './enhancer' +import type { CommentTableRow } from '@/entrypoints/background' +import type { CommentEvent } from './enhancer' // Message handler response types export const CLOSE_MESSAGE_PORT = false as const // No response will be sent @@ -24,15 +25,8 @@ export type PopupToBackgroundMessage = GetOpenSpotsMessage | SwitchToTabMessage export type ToBackgroundMessage = ContentToBackgroundMessage | PopupToBackgroundMessage // Background -> Popup responses -export interface GetOpenSpotsResponse { - spots: Array<{ - tab: { - tabId: number - windowId: number - } - spot: CommentSpot - drafts: Array<[number, string]> - }> +export interface GetTableRowsResponse { + rows: CommentTableRow[] } // Type guard functions @@ -76,5 +70,5 @@ export type BackgroundMessageHandler = ( export type PopupMessageSender = { sendMessage( message: T, - ): Promise + ): Promise } diff --git a/browser-extension/tests/background.test.ts b/browser-extension/tests/background.test.ts index 961e1d0..2aa8a3d 100644 --- a/browser-extension/tests/background.test.ts +++ b/browser-extension/tests/background.test.ts @@ -31,6 +31,7 @@ describe('Background Event Handler', () => { "test-key", { "drafts": [], + "sentOn": null, "spot": { "type": "TEST_SPOT", "unique_key": "test-key", @@ -39,6 +40,7 @@ describe('Background Event Handler', () => { "tabId": 123, "windowId": 456, }, + "trashedOn": null, }, ], ] diff --git a/browser-extension/tests/playground/replica.tsx b/browser-extension/tests/playground/replica.tsx index 37e013f..98ba466 100644 --- a/browser-extension/tests/playground/replica.tsx +++ b/browser-extension/tests/playground/replica.tsx @@ -1,5 +1,5 @@ import { SpotTable } from '@/components/SpotTable' -import type { CommentState } from '@/entrypoints/background' +import type { CommentStorage } from '@/entrypoints/background' import type { CommentSpot } from '@/lib/enhancer' import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' @@ -23,20 +23,22 @@ const gh_issue: GitHubIssueAddCommentSpot = { } const spots: CommentSpot[] = [gh_pr, gh_issue] -const sampleSpots: CommentState[] = spots.map((spot) => { - const state: CommentState = { +const sampleSpots: CommentStorage[] = spots.map((spot) => { + const state: CommentStorage = { drafts: [[0, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.']], + sentOn: null, spot, tab: { tabId: 123, windowId: 456, }, + trashedOn: null, } return state }) export function Replica() { - const handleSpotClick = (spot: CommentState) => { + const handleSpotClick = (spot: CommentStorage) => { alert(`Clicked: ${spot.spot.type}\nTab: ${spot.tab.tabId}`) } From 5d37a2718a8e5486a4c9963e123de5fc0fdde620 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 14:10:55 -0700 Subject: [PATCH 100/109] Fully complete the wiring. --- .../src/components/PopupRoot.tsx | 310 ++++++++++++++++++ browser-extension/src/components/SpotRow.tsx | 42 --- .../src/components/SpotTable.tsx | 78 ----- .../src/components/ui/button.tsx | 49 --- browser-extension/src/components/ui/table.tsx | 91 ----- .../src/entrypoints/popup/popup.tsx | 66 ++-- .../tests/playground/replica.tsx | 48 ++- 7 files changed, 354 insertions(+), 330 deletions(-) create mode 100644 browser-extension/src/components/PopupRoot.tsx delete mode 100644 browser-extension/src/components/SpotRow.tsx delete mode 100644 browser-extension/src/components/SpotTable.tsx delete mode 100644 browser-extension/src/components/ui/button.tsx delete mode 100644 browser-extension/src/components/ui/table.tsx diff --git a/browser-extension/src/components/PopupRoot.tsx b/browser-extension/src/components/PopupRoot.tsx new file mode 100644 index 0000000..b7f1203 --- /dev/null +++ b/browser-extension/src/components/PopupRoot.tsx @@ -0,0 +1,310 @@ +import { Eye, EyeOff, Search, Settings, Trash2 } from 'lucide-react' +import { useMemo, useState } from 'react' +import { twMerge } from 'tailwind-merge' +import Badge from '@/components/Badge' +import { badgeCVA } from '@/components/design' +import MultiSegment from '@/components/MultiSegment' +import { allLeafValues, timeAgo } from '@/components/misc' +import type { CommentTableRow } from '@/entrypoints/background' +import type { FilterState } from '@/entrypoints/popup/popup' +import { EnhancerRegistry } from '@/lib/registries' + +const initialFilter: FilterState = { + searchQuery: '', + sentFilter: 'both', + showTrashed: false, +} + +interface PopupRootProps { + drafts: CommentTableRow[] +} + +export function PopupRoot({ drafts }: PopupRootProps) { + const [selectedIds, setSelectedIds] = useState(new Set()) + const [filters, setFilters] = useState(initialFilter) + + const updateFilter = (key: K, value: FilterState[K]) => { + setFilters((prev) => ({ ...prev, [key]: value })) + } + + const filteredDrafts = useMemo(() => { + let filtered = [...drafts] + if (!filters.showTrashed) { + filtered = filtered.filter((d) => !d.isTrashed) + } + if (filters.sentFilter !== 'both') { + filtered = filtered.filter((d) => (filters.sentFilter === 'sent' ? d.isSent : !d.isSent)) + } + if (filters.searchQuery) { + const query = filters.searchQuery.toLowerCase() + filtered = filtered.filter((d) => { + for (const value of allLeafValues(d)) { + if (value.toLowerCase().includes(query)) { + return true // Early exit on first match + } + } + return false + }) + } + // sort by newest + filtered.sort((a, b) => b.latestDraft.time - a.latestDraft.time) + return filtered + }, [drafts, filters]) + + const toggleSelection = (id: string) => { + const newSelected = new Set(selectedIds) + if (newSelected.has(id)) { + newSelected.delete(id) + } else { + newSelected.add(id) + } + setSelectedIds(newSelected) + } + + const toggleSelectAll = () => { + if (selectedIds.size === filteredDrafts.length && filteredDrafts.length > 0) { + setSelectedIds(new Set()) + } else { + setSelectedIds(new Set(filteredDrafts.map((d) => d.spot.unique_key))) + } + } + + const handleOpen = (url: string) => { + window.open(url, '_blank') + } + + const handleTrash = (row: CommentTableRow) => { + if (row.latestDraft.stats.charCount > 20) { + if (confirm('Are you sure you want to discard this draft?')) { + console.log('Trashing draft:', row.spot.unique_key) + } + } else { + console.log('Trashing draft:', row.spot.unique_key) + } + } + + const clearFilters = () => { + setFilters({ + searchQuery: '', + sentFilter: 'both', + showTrashed: true, + }) + } + + const getTableBody = () => { + if (drafts.length === 0) { + return + } + + if (filteredDrafts.length === 0 && (filters.searchQuery || filters.sentFilter !== 'both')) { + return + } + + return filteredDrafts.map((row) => + commentRow(row, selectedIds, toggleSelection, handleOpen, handleTrash), + ) + } + + return ( +
    + {/* Bulk actions bar - floating popup */} + {selectedIds.size > 0 && ( +
    + {selectedIds.size} selected + + + + +
    + )} + + {/* Table */} +
    + + + + + + + + + + + + {getTableBody()} +
    + 0} + onChange={toggleSelectAll} + aria-label='Select all' + className='rounded' + /> + +
    +
    +
    + + updateFilter('searchQuery', e.target.value)} + className='w-full pl-5 pr-3 h-5 border border-gray-300 rounded-sm text-sm font-normal focus:outline-none focus:border-blue-500' + /> +
    +
    + + + value={filters.sentFilter} + onValueChange={(value) => updateFilter('sentFilter', value)} + segments={[ + { + text: '', + type: 'unsent', + value: 'unsent', + }, + { + text: 'both', + type: 'blank', + value: 'both', + }, + { + text: '', + type: 'sent', + value: 'sent', + }, + ]} + /> + +
    +
    +
    +
    +
    +
    + ) +} + +const enhancers = new EnhancerRegistry() +function commentRow( + row: CommentTableRow, + selectedIds: Set, + toggleSelection: (id: string) => void, + _handleOpen: (url: string) => void, + _handleTrash: (row: CommentTableRow) => void, +) { + const enhancer = enhancers.enhancerFor(row.spot) + return ( + + + toggleSelection(row.spot.unique_key)} + className='rounded' + /> + + +
    + {/* Context line */} +
    +
    + {enhancer.tableUpperDecoration(row.spot)} +
    +
    + {row.latestDraft.stats.links.length > 0 && ( + + )} + {row.latestDraft.stats.images.length > 0 && ( + + )} + {row.latestDraft.stats.codeBlocks.length > 0 && ( + + )} + + + {row.isOpenTab && } +
    +
    + + {/* Title */} +
    + + {enhancer.tableTitle(row.spot)} + + + {row.isTrashed && } +
    + {/* Draft */} +
    + {row.latestDraft.content.substring(0, 100)}… +
    +
    + + + ) +} + +const EmptyState = () => ( +
    +

    No comments open

    +

    + Your drafts will appear here when you start typing in comment boxes across GitHub and Reddit. +

    +
    + + · + +
    +
    +) + +const NoMatchesState = ({ onClearFilters }: { onClearFilters: () => void }) => ( +
    +

    No matches found

    + +
    +) diff --git a/browser-extension/src/components/SpotRow.tsx b/browser-extension/src/components/SpotRow.tsx deleted file mode 100644 index fe58847..0000000 --- a/browser-extension/src/components/SpotRow.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { TableCell, TableRow } from '@/components/ui/table' -import type { CommentStorage } from '@/entrypoints/background' -import type { EnhancerRegistry } from '@/lib/registries' -import { cn } from '@/lib/utils' - -interface SpotRowProps { - commentState: CommentStorage - enhancerRegistry: EnhancerRegistry - onClick: () => void - className?: string - cellClassName?: string - errorClassName?: string -} - -export function SpotRow({ - commentState, - enhancerRegistry, - onClick, - className, - cellClassName = 'p-3', - errorClassName = 'text-red-500', -}: SpotRowProps) { - const enhancer = enhancerRegistry.enhancerFor(commentState.spot) - - if (!enhancer) { - return ( - - -
    Unknown spot type: {commentState.spot.type}
    -
    -
    - ) - } - - return ( - - - {enhancer.tableUpperDecoration(commentState.spot)} - - - ) -} diff --git a/browser-extension/src/components/SpotTable.tsx b/browser-extension/src/components/SpotTable.tsx deleted file mode 100644 index 1c02a09..0000000 --- a/browser-extension/src/components/SpotTable.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import type { CommentStorage } from '@/entrypoints/background' -import type { EnhancerRegistry } from '@/lib/registries' -import { SpotRow } from './SpotRow' - -interface SpotTableProps { - spots: CommentStorage[] - enhancerRegistry: EnhancerRegistry - onSpotClick: (spot: CommentStorage) => void - title?: string - description?: string - headerText?: string - className?: string - headerClassName?: string - rowClassName?: string - cellClassName?: string - emptyStateMessage?: string - showHeader?: boolean -} - -export function SpotTable({ - spots, - enhancerRegistry, - onSpotClick, - title, - description, - headerText = 'Comment Spots', - className, - headerClassName = 'p-3 font-medium text-muted-foreground', - rowClassName, - cellClassName, - emptyStateMessage = 'No comment spots available', - showHeader = true, -}: SpotTableProps) { - if (spots.length === 0) { - return
    {emptyStateMessage}
    - } - - const tableContent = ( - - {showHeader && ( - - - {headerText} - - - )} - - {spots.map((spot) => ( - onSpotClick(spot)} - className={rowClassName || ''} - cellClassName={cellClassName || 'p-3'} - /> - ))} - -
    - ) - - if (title || description) { - return ( -
    - {(title || description) && ( -
    - {title &&

    {title}

    } - {description &&

    {description}

    } -
    - )} - {tableContent} -
    - ) - } - - return
    {tableContent}
    -} diff --git a/browser-extension/src/components/ui/button.tsx b/browser-extension/src/components/ui/button.tsx deleted file mode 100644 index 80f6691..0000000 --- a/browser-extension/src/components/ui/button.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' -import * as React from 'react' - -import { cn } from '@/lib/utils' - -const buttonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', - { - defaultVariants: { - size: 'default', - variant: 'default', - }, - variants: { - size: { - default: 'h-10 px-4 py-2', - icon: 'h-10 w-10', - lg: 'h-11 rounded-md px-8', - sm: 'h-9 rounded-md px-3', - }, - variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', - outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - }, - }, - }, -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button' - return ( - - ) - }, -) -Button.displayName = 'Button' - -export { Button, buttonVariants } diff --git a/browser-extension/src/components/ui/table.tsx b/browser-extension/src/components/ui/table.tsx deleted file mode 100644 index e8548bf..0000000 --- a/browser-extension/src/components/ui/table.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react' - -import { cn } from '@/lib/utils' - -const Table = React.forwardRef>( - ({ className, ...props }, ref) => ( -
    - - - ), -) -Table.displayName = 'Table' - -const TableHeader = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)) -TableHeader.displayName = 'TableHeader' - -const TableBody = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)) -TableBody.displayName = 'TableBody' - -const TableFooter = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - tr]:last:border-b-0', className)} - {...props} - /> -)) -TableFooter.displayName = 'TableFooter' - -const TableRow = React.forwardRef>( - ({ className, ...props }, ref) => ( - - ), -) -TableRow.displayName = 'TableRow' - -const TableHead = React.forwardRef< - HTMLTableCellElement, - React.ThHTMLAttributes ->(({ className, ...props }, ref) => ( - - + - - + -
    -)) -TableHead.displayName = 'TableHead' - -const TableCell = React.forwardRef< - HTMLTableCellElement, - React.TdHTMLAttributes ->(({ className, ...props }, ref) => ( - -)) -TableCell.displayName = 'TableCell' - -const TableCaption = React.forwardRef< - HTMLTableCaptionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)) -TableCaption.displayName = 'TableCaption' - -export { Table, TableHeader, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableRow } diff --git a/browser-extension/src/entrypoints/popup/popup.tsx b/browser-extension/src/entrypoints/popup/popup.tsx index dd55d76..72a8756 100644 --- a/browser-extension/src/entrypoints/popup/popup.tsx +++ b/browser-extension/src/entrypoints/popup/popup.tsx @@ -1,10 +1,16 @@ import './style.css' -import React from 'react' import { createRoot } from 'react-dom/client' +import { PopupRoot } from '@/components/PopupRoot' import type { CommentTableRow } from '@/entrypoints/background' import { logger } from '@/lib/logger' import type { GetOpenSpotsMessage, GetTableRowsResponse } from '@/lib/messages' +export interface FilterState { + sentFilter: 'both' | 'sent' | 'unsent' + searchQuery: string + showTrashed: boolean +} + async function getOpenSpots(): Promise { logger.debug('Sending message to background script...') try { @@ -28,54 +34,24 @@ async function getOpenSpots(): Promise { // window.close() // } -export interface FilterState { - sentFilter: 'both' | 'sent' | 'unsent' - searchQuery: string - showTrashed: boolean -} - -function PopupApp() { - const [_spots, setSpots] = React.useState([]) - const [isLoading, setIsLoading] = React.useState(true) - - React.useEffect(() => { - const loadSpots = async () => { - try { - const openSpots = await getOpenSpots() - setSpots(openSpots) - } catch (error) { - logger.error('Error loading spots:', error) - } finally { - setIsLoading(false) - } - } - - loadSpots() - }, []) - - if (isLoading) { - return
    Loading...
    - } - - // const handleSpotClick = (spot: CommentTableRow) => { - // console.log('TODO: switchToTab') - // //switchToTab(spot.tab.tabId, spot.tab.windowId) - // } - - return ( -
    -

    Open Comment Spots

    - -
    -
    - ) -} +// const handleSpotClick = (spot: CommentTableRow) => { +// console.log('TODO: switchToTab') +// //switchToTab(spot.tab.tabId, spot.tab.windowId) +// } -// Initialize React app const app = document.getElementById('app') if (app) { const root = createRoot(app) - root.render() + + // Load initial data and render + getOpenSpots() + .then((drafts) => { + root.render() + }) + .catch((error) => { + logger.error('Failed to load initial data:', error) + root.render() + }) } else { logger.error('App element not found') } diff --git a/browser-extension/tests/playground/replica.tsx b/browser-extension/tests/playground/replica.tsx index 98ba466..71c1dfe 100644 --- a/browser-extension/tests/playground/replica.tsx +++ b/browser-extension/tests/playground/replica.tsx @@ -1,9 +1,8 @@ -import { SpotTable } from '@/components/SpotTable' -import type { CommentStorage } from '@/entrypoints/background' +import { PopupRoot } from '@/components/PopupRoot' +import type { CommentStorage, CommentTableRow } from '@/entrypoints/background' import type { CommentSpot } from '@/lib/enhancer' import type { GitHubIssueAddCommentSpot } from '@/lib/enhancers/github/githubIssueAddComment' import type { GitHubPRAddCommentSpot } from '@/lib/enhancers/github/githubPRAddComment' -import { EnhancerRegistry } from '@/lib/registries' const gh_pr: GitHubPRAddCommentSpot = { domain: 'github.com', @@ -38,28 +37,27 @@ const sampleSpots: CommentStorage[] = spots.map((spot) => { }) export function Replica() { - const handleSpotClick = (spot: CommentStorage) => { - alert(`Clicked: ${spot.spot.type}\nTab: ${spot.tab.tabId}`) - } - - const enhancers = new EnhancerRegistry() - return ( -
    -

    Open Comment Spots

    - -
    - -
    -
    + { + const row: CommentTableRow = { + isOpenTab: true, + isSent: true, + isTrashed: false, + latestDraft: { + content: 'lorum ipsum', + stats: { + charCount: 99, + codeBlocks: [], + images: [], + links: [], + }, + time: 0, + }, + spot: storage.spot, + } + return row + })} + > ) } From 03eb9649ddf01637ba1e773bc4f73abbbdc12aec Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 15:10:02 -0700 Subject: [PATCH 101/109] This worked. --- browser-extension/components.json | 21 --------------------- browser-extension/vite.playground.config.ts | 1 - 2 files changed, 22 deletions(-) delete mode 100644 browser-extension/components.json diff --git a/browser-extension/components.json b/browser-extension/components.json deleted file mode 100644 index cd33c15..0000000 --- a/browser-extension/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/styles/globals.css", - "baseColor": "slate", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} \ No newline at end of file diff --git a/browser-extension/vite.playground.config.ts b/browser-extension/vite.playground.config.ts index 73ff163..0bb1188 100644 --- a/browser-extension/vite.playground.config.ts +++ b/browser-extension/vite.playground.config.ts @@ -14,7 +14,6 @@ export default defineConfig({ '@': path.resolve('./src'), }, }, - root: 'tests/playground', server: { host: true, open: true, From 45201b4802d8fac4f7922f0e693d4f36a26241aa Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 16:12:29 -0700 Subject: [PATCH 102/109] Give `playground-styles.css` a distinct name to avoid confusion. --- .../playground/{style.css => playground-styles.css} | 0 browser-extension/tests/playground/playground.tsx | 11 +++++------ 2 files changed, 5 insertions(+), 6 deletions(-) rename browser-extension/tests/playground/{style.css => playground-styles.css} (100%) diff --git a/browser-extension/tests/playground/style.css b/browser-extension/tests/playground/playground-styles.css similarity index 100% rename from browser-extension/tests/playground/style.css rename to browser-extension/tests/playground/playground-styles.css diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 0351164..244ec00 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { createRoot } from 'react-dom/client' import '@/entrypoints/popup/style.css' -import './style.css' +import './playground-styles.css' import { ClaudePrototype } from './claude' import { Replica } from './replica' @@ -30,11 +30,10 @@ const App = () => { key={mode} type='button' onClick={() => setActiveComponent(mode as Mode)} - className={`px-3 py-2 rounded text-sm font-medium transition-colors ${ - activeComponent === mode - ? 'bg-blue-600 text-white' - : 'bg-gray-100 text-gray-700 hover:bg-gray-200' - }`} + className={`px-3 py-2 rounded text-sm font-medium transition-colors ${activeComponent === mode + ? 'bg-blue-600 text-white' + : 'bg-gray-100 text-gray-700 hover:bg-gray-200' + }`} > {config.label} From 461bec34f104647f141d994bbdc2664b8946b61e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 16:16:34 -0700 Subject: [PATCH 103/109] Compress unnecessary css splitting. --- .../src/entrypoints/popup/style.css | 16 +++++++++++++++- browser-extension/src/styles/globals.css | 1 - browser-extension/src/styles/popup-frame.css | 15 --------------- .../tests/playground/playground.tsx | 9 +++++---- 4 files changed, 20 insertions(+), 21 deletions(-) delete mode 100644 browser-extension/src/styles/globals.css delete mode 100644 browser-extension/src/styles/popup-frame.css diff --git a/browser-extension/src/entrypoints/popup/style.css b/browser-extension/src/entrypoints/popup/style.css index 9cbe0f6..d9385b9 100644 --- a/browser-extension/src/entrypoints/popup/style.css +++ b/browser-extension/src/entrypoints/popup/style.css @@ -1,2 +1,16 @@ -@import url("../../styles/popup-frame.css"); @import "tailwindcss"; +/* Popup window frame styles */ +:root { + --popup-width: 600px; + --popup-height: 400px; +} + +body { + width: var(--popup-width); + height: var(--popup-height); + padding: 15px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + line-height: 1.4; + margin: 0; +} diff --git a/browser-extension/src/styles/globals.css b/browser-extension/src/styles/globals.css deleted file mode 100644 index f1d8c73..0000000 --- a/browser-extension/src/styles/globals.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/browser-extension/src/styles/popup-frame.css b/browser-extension/src/styles/popup-frame.css deleted file mode 100644 index f2f2c0a..0000000 --- a/browser-extension/src/styles/popup-frame.css +++ /dev/null @@ -1,15 +0,0 @@ -/* Popup window frame styles */ -:root { - --popup-width: 600px; - --popup-height: 400px; -} - -body { - width: var(--popup-width); - height: var(--popup-height); - padding: 15px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - font-size: 14px; - line-height: 1.4; - margin: 0; -} diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index 244ec00..b21a6ca 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -30,10 +30,11 @@ const App = () => { key={mode} type='button' onClick={() => setActiveComponent(mode as Mode)} - className={`px-3 py-2 rounded text-sm font-medium transition-colors ${activeComponent === mode - ? 'bg-blue-600 text-white' - : 'bg-gray-100 text-gray-700 hover:bg-gray-200' - }`} + className={`px-3 py-2 rounded text-sm font-medium transition-colors ${ + activeComponent === mode + ? 'bg-blue-600 text-white' + : 'bg-gray-100 text-gray-700 hover:bg-gray-200' + }`} > {config.label} From 43c4920b4a94808e167a68e19ecbbecc840cb21d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 16:20:14 -0700 Subject: [PATCH 104/109] Put the playground back into the weird root, but fix tailwind scanning. --- browser-extension/src/entrypoints/popup/popup.tsx | 2 +- browser-extension/src/entrypoints/popup/style.css | 2 +- browser-extension/vite.playground.config.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/browser-extension/src/entrypoints/popup/popup.tsx b/browser-extension/src/entrypoints/popup/popup.tsx index 72a8756..7788059 100644 --- a/browser-extension/src/entrypoints/popup/popup.tsx +++ b/browser-extension/src/entrypoints/popup/popup.tsx @@ -17,7 +17,7 @@ async function getOpenSpots(): Promise { const message: GetOpenSpotsMessage = { type: 'GET_OPEN_SPOTS' } const response = (await browser.runtime.sendMessage(message)) as GetTableRowsResponse logger.debug('Received response:', response) - return response.rows || [] + return response.rows } catch (error) { logger.error('Error sending message to background:', error) return [] diff --git a/browser-extension/src/entrypoints/popup/style.css b/browser-extension/src/entrypoints/popup/style.css index d9385b9..c05cef7 100644 --- a/browser-extension/src/entrypoints/popup/style.css +++ b/browser-extension/src/entrypoints/popup/style.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import "tailwindcss" source("../../.."); /* Popup window frame styles */ :root { --popup-width: 600px; diff --git a/browser-extension/vite.playground.config.ts b/browser-extension/vite.playground.config.ts index 0bb1188..73ff163 100644 --- a/browser-extension/vite.playground.config.ts +++ b/browser-extension/vite.playground.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ '@': path.resolve('./src'), }, }, + root: 'tests/playground', server: { host: true, open: true, From 701e16fdeb556f296cbd817b08798281395c0bd1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 16:32:57 -0700 Subject: [PATCH 105/109] Modify tailwind imports in a way that biome can handle. --- browser-extension/src/entrypoints/popup/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/browser-extension/src/entrypoints/popup/style.css b/browser-extension/src/entrypoints/popup/style.css index c05cef7..d718f6e 100644 --- a/browser-extension/src/entrypoints/popup/style.css +++ b/browser-extension/src/entrypoints/popup/style.css @@ -1,4 +1,5 @@ -@import "tailwindcss" source("../../.."); +@import "tailwindcss"; +@source "../../.."; /* Popup window frame styles */ :root { --popup-width: 600px; From eb04c0e4a2dc08d03c0773587aa640095a3dd7f6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 16:41:06 -0700 Subject: [PATCH 106/109] Unneeded. --- browser-extension/src/lib/utils.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 browser-extension/src/lib/utils.ts diff --git a/browser-extension/src/lib/utils.ts b/browser-extension/src/lib/utils.ts deleted file mode 100644 index d32b0fe..0000000 --- a/browser-extension/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from 'clsx' -import { twMerge } from 'tailwind-merge' - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} From b7329854eab3d86abaf4d2ed7f23bea0a854afb8 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 16:44:30 -0700 Subject: [PATCH 107/109] Swap `class-variance-authority` and `clsx` for `tailwind-variants`. --- browser-extension/package.json | 4 +- browser-extension/src/components/Badge.tsx | 2 +- browser-extension/src/components/design.tsx | 60 +++++++++-------- pnpm-lock.yaml | 72 ++++++--------------- 4 files changed, 50 insertions(+), 88 deletions(-) diff --git a/browser-extension/package.json b/browser-extension/package.json index a419171..9097cba 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -2,18 +2,16 @@ "author": "DiffPlug", "dependencies": { "@primer/octicons-react": "^19.18.0", - "@radix-ui/react-slot": "^1.2.3", "@types/react": "^19.1.12", "@types/react-dom": "^19.1.9", "@wxt-dev/webextension-polyfill": "^1.0.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "highlight.js": "^11.11.1", "lucide-react": "^0.543.0", "overtype": "workspace:*", "react": "^19.1.1", "react-dom": "^19.1.1", "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.1.1", "webextension-polyfill": "^0.12.0" }, "description": "Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).", diff --git a/browser-extension/src/components/Badge.tsx b/browser-extension/src/components/Badge.tsx index 68dc2bf..0f2d89e 100644 --- a/browser-extension/src/components/Badge.tsx +++ b/browser-extension/src/components/Badge.tsx @@ -1,5 +1,5 @@ -import type { VariantProps } from 'class-variance-authority' import { twMerge } from 'tailwind-merge' +import type { VariantProps } from 'tailwind-variants' import { badgeCVA, typeIcons } from '@/components/design' export type BadgeProps = VariantProps & { diff --git a/browser-extension/src/components/design.tsx b/browser-extension/src/components/design.tsx index f08f191..e99ef68 100644 --- a/browser-extension/src/components/design.tsx +++ b/browser-extension/src/components/design.tsx @@ -1,4 +1,3 @@ -import { cva } from 'class-variance-authority' import { Clock, Code, @@ -12,40 +11,39 @@ import { TextSelect, Trash2, } from 'lucide-react' +import { tv } from 'tailwind-variants' -// CVA configuration for stat badges -export const badgeCVA = cva( - 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal h-5', - { - defaultVariants: { - clickable: false, +// TV configuration for stat badges +export const badgeCVA = tv({ + base: 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal h-5', + defaultVariants: { + clickable: false, + }, + variants: { + clickable: { + false: '', + true: 'cursor-pointer border border-transparent hover:border-current border-dashed', + }, + selected: { + false: '', + true: '!border-solid !border-current', }, - variants: { - clickable: { - false: '', - true: 'cursor-pointer border border-transparent hover:border-current border-dashed', - }, - selected: { - false: '', - true: '!border-solid !border-current', - }, - type: { - blank: 'bg-transparent text-gray-700', - code: 'bg-pink-50 text-pink-700', - hideTrashed: 'bg-transparent text-gray-700', - image: 'bg-purple-50 text-purple-700', - link: 'bg-blue-50 text-blue-700', - open: 'bg-cyan-50 text-cyan-700', - sent: 'bg-green-50 text-green-700', - settings: 'bg-gray-50 text-gray-700', - text: 'bg-gray-50 text-gray-700', - time: 'bg-gray-50 text-gray-700', - trashed: 'bg-gray-50 text-yellow-700', - unsent: 'bg-amber-100 text-amber-700', - }, + type: { + blank: 'bg-transparent text-gray-700', + code: 'bg-pink-50 text-pink-700', + hideTrashed: 'bg-transparent text-gray-700', + image: 'bg-purple-50 text-purple-700', + link: 'bg-blue-50 text-blue-700', + open: 'bg-cyan-50 text-cyan-700', + sent: 'bg-green-50 text-green-700', + settings: 'bg-gray-50 text-gray-700', + text: 'bg-gray-50 text-gray-700', + time: 'bg-gray-50 text-gray-700', + trashed: 'bg-gray-50 text-yellow-700', + unsent: 'bg-amber-100 text-amber-700', }, }, -) +}) // Map types to their icons export const typeIcons = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55dc316..f05be97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,9 +13,6 @@ importers: '@primer/octicons-react': specifier: ^19.18.0 version: 19.18.0(react@19.1.1) - '@radix-ui/react-slot': - specifier: ^1.2.3 - version: 1.2.3(@types/react@19.1.12)(react@19.1.1) '@types/react': specifier: ^19.1.12 version: 19.1.12 @@ -25,12 +22,6 @@ importers: '@wxt-dev/webextension-polyfill': specifier: ^1.0.0 version: 1.0.0(webextension-polyfill@0.12.0)(wxt@0.20.11(@types/node@22.18.1)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.50.1)(tsx@4.20.5)) - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -49,6 +40,9 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.3.1 + tailwind-variants: + specifier: ^3.1.1 + version: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.13) webextension-polyfill: specifier: ^0.12.0 version: 0.12.0 @@ -715,24 +709,6 @@ packages: peerDependencies: react: '>=16.3' - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@rolldown/pluginutils@1.0.0-beta.34': resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} @@ -1300,9 +1276,6 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -1339,10 +1312,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -3072,6 +3041,16 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-variants@3.1.1: + resolution: {integrity: sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + tailwindcss@4.1.13: resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} @@ -3936,19 +3915,6 @@ snapshots: dependencies: react: 19.1.1 - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.12)(react@19.1.1)': - dependencies: - react: 19.1.1 - optionalDependencies: - '@types/react': 19.1.12 - - '@radix-ui/react-slot@1.2.3(@types/react@19.1.12)(react@19.1.1)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - optionalDependencies: - '@types/react': 19.1.12 - '@rolldown/pluginutils@1.0.0-beta.34': {} '@rollup/rollup-android-arm-eabi@4.50.1': @@ -4529,10 +4495,6 @@ snapshots: dependencies: consola: 3.4.2 - class-variance-authority@0.7.1: - dependencies: - clsx: 2.1.1 - cli-boxes@3.0.0: {} cli-cursor@4.0.0: @@ -4573,8 +4535,6 @@ snapshots: clone@1.0.4: {} - clsx@2.1.1: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -6336,6 +6296,12 @@ snapshots: tailwind-merge@3.3.1: {} + tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.13): + dependencies: + tailwindcss: 4.1.13 + optionalDependencies: + tailwind-merge: 3.3.1 + tailwindcss@4.1.13: {} tapable@2.2.3: {} From 965e8a4e2a8426ab49573d0d9f708b7fa8884847 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 16 Sep 2025 16:49:46 -0700 Subject: [PATCH 108/109] Enable tailwind class sorting. --- browser-extension/biome.json | 3 ++ browser-extension/src/components/Badge.tsx | 2 +- .../src/components/MultiSegment.tsx | 2 +- .../src/components/PopupRoot.tsx | 50 +++++++++---------- .../lib/enhancers/CommentEnhancerMissing.tsx | 6 +-- .../github/githubIssueAddComment.tsx | 4 +- .../github/githubIssueNewComment.tsx | 2 +- .../enhancers/github/githubPRAddComment.tsx | 2 +- .../enhancers/github/githubPRNewComment.tsx | 2 +- browser-extension/tests/playground/claude.tsx | 50 +++++++++---------- .../tests/playground/playground.tsx | 10 ++-- 11 files changed, 68 insertions(+), 65 deletions(-) diff --git a/browser-extension/biome.json b/browser-extension/biome.json index 3d17dc5..1056f6b 100644 --- a/browser-extension/biome.json +++ b/browser-extension/biome.json @@ -47,6 +47,9 @@ "noUnusedVariables": "error", "useValidTypeof": "error" }, + "nursery": { + "useSortedClasses": "error" + }, "recommended": true, "style": { "noDefaultExport": "off", diff --git a/browser-extension/src/components/Badge.tsx b/browser-extension/src/components/Badge.tsx index 0f2d89e..2f64a53 100644 --- a/browser-extension/src/components/Badge.tsx +++ b/browser-extension/src/components/Badge.tsx @@ -17,7 +17,7 @@ const Badge = ({ text, type }: BadgeProps) => { }), )} > - {type === 'blank' || } + {type === 'blank' || } {text || type} ) diff --git a/browser-extension/src/components/MultiSegment.tsx b/browser-extension/src/components/MultiSegment.tsx index d075e39..62cc078 100644 --- a/browser-extension/src/components/MultiSegment.tsx +++ b/browser-extension/src/components/MultiSegment.tsx @@ -39,7 +39,7 @@ const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps< onClick={() => onValueChange(segment.value)} type='button' > - {segment.type === 'blank' || } + {segment.type === 'blank' || } {segment.text} ) diff --git a/browser-extension/src/components/PopupRoot.tsx b/browser-extension/src/components/PopupRoot.tsx index b7f1203..ec32e04 100644 --- a/browser-extension/src/components/PopupRoot.tsx +++ b/browser-extension/src/components/PopupRoot.tsx @@ -109,18 +109,18 @@ export function PopupRoot({ drafts }: PopupRootProps) {
    {/* Bulk actions bar - floating popup */} {selectedIds.size > 0 && ( -
    - {selectedIds.size} selected - - - -
    @@ -133,7 +133,7 @@ export function PopupRoot({ drafts }: PopupRootProps) {
    +
    - + updateFilter('searchQuery', e.target.value)} - className='w-full pl-5 pr-3 h-5 border border-gray-300 rounded-sm text-sm font-normal focus:outline-none focus:border-blue-500' + className='h-5 w-full rounded-sm border border-gray-300 pr-3 pl-5 font-normal text-sm focus:border-blue-500 focus:outline-none' />
    -
    +
    @@ -207,7 +207,7 @@ export function PopupRoot({ drafts }: PopupRootProps) { 'border', )} > - +
    @@ -244,11 +244,11 @@ function commentRow(
    {/* Context line */} -
    -
    +
    +
    {enhancer.tableUpperDecoration(row.spot)}
    -
    +
    {row.latestDraft.stats.links.length > 0 && ( )} @@ -266,14 +266,14 @@ function commentRow( {/* Title */} {/* Draft */} -
    +
    {row.latestDraft.content.substring(0, 100)}…
    @@ -283,9 +283,9 @@ function commentRow( } const EmptyState = () => ( -
    -

    No comments open

    -

    +

    +

    No comments open

    +

    Your drafts will appear here when you start typing in comment boxes across GitHub and Reddit.

    @@ -301,8 +301,8 @@ const EmptyState = () => ( ) const NoMatchesState = ({ onClearFilters }: { onClearFilters: () => void }) => ( -
    -

    No matches found

    +
    +

    No matches found

    diff --git a/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx b/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx index 7c01a35..a470feb 100644 --- a/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx +++ b/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx @@ -8,7 +8,7 @@ export class CommentEnhancerMissing implements CommentEnhancer { return ( - - -
    @@ -129,7 +129,7 @@ export const ClaudePrototype = () => {
    { className='rounded' /> +
    - + updateFilter('searchQuery', e.target.value)} - className='w-full pl-5 pr-3 h-5 border border-gray-300 rounded-sm text-sm font-normal focus:outline-none focus:border-blue-500' + className='h-5 w-full rounded-sm border border-gray-300 pr-3 pl-5 font-normal text-sm focus:border-blue-500 focus:outline-none' />
    -
    +
    @@ -203,7 +203,7 @@ export const ClaudePrototype = () => { 'border', )} > - +
    @@ -240,11 +240,11 @@ function commentRow(
    {/* Context line */} -
    -
    +
    +
    {enhancer.tableUpperDecoration(row.spot)}
    -
    +
    {row.latestDraft.stats.links.length > 0 && ( )} @@ -262,14 +262,14 @@ function commentRow( {/* Title */} {/* Draft */} -
    +
    {row.latestDraft.content.substring(0, 100)}…
    @@ -279,9 +279,9 @@ function commentRow( } const EmptyState = () => ( -
    -

    No comments open

    -

    +

    +

    No comments open

    +

    Your drafts will appear here when you start typing in comment boxes across GitHub and Reddit.

    @@ -297,8 +297,8 @@ const EmptyState = () => ( ) const NoMatchesState = ({ onClearFilters }: { onClearFilters: () => void }) => ( -
    -

    No matches found

    +
    +

    No matches found

    diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx index b21a6ca..fc32087 100644 --- a/browser-extension/tests/playground/playground.tsx +++ b/browser-extension/tests/playground/playground.tsx @@ -18,19 +18,19 @@ const App = () => { return (
    -
    -

    Popup Simulator

    -
      +
      +

      Popup Simulator

      +
      • The popup frame is meant to exactly match the browser extension popup.
      • Hot reload is active for instant updates
      -
      +
      {Object.entries(MODES).map(([mode, config]) => (