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/
diff --git a/browser-extension/README.md b/browser-extension/README.md
index cb2641f..79275aa 100644
--- a/browser-extension/README.md
+++ b/browser-extension/README.md
@@ -33,15 +33,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` - html/css/ts which opens when the extension's button gets clicked
+- [`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.ts]
+ Popup[Popup Script
popup/popup.tsx]
Content -->|ENHANCED/DESTROYED
CommentEvent| Background
Popup -->|GET_OPEN_SPOTS
SWITCH_TO_TAB| Background
@@ -60,22 +60,20 @@ 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.
-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`].
+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.
-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).
-
-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
-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
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"
}
}
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 128aa79..e2eabb0 100644
--- a/browser-extension/package.json
+++ b/browser-extension/package.json
@@ -1,25 +1,40 @@
{
"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",
"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/vite": "^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",
"express": "^4.19.2",
"linkedom": "^0.18.12",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.13",
"tsx": "^4.19.1",
"typescript": "^5.8.3",
+ "vite": "^7.1.5",
"vitest": "^3.2.4",
"wxt": "^0.20.7"
},
@@ -47,6 +62,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/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/components/ui/button.tsx b/browser-extension/src/components/ui/button.tsx
new file mode 100644
index 0000000..80f6691
--- /dev/null
+++ b/browser-extension/src/components/ui/button.tsx
@@ -0,0 +1,49 @@
+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
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) => (
+ |
+))
+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/background.ts b/browser-extension/src/entrypoints/background.ts
index 3e071a1..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
@@ -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
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/index.html b/browser-extension/src/entrypoints/popup/index.html
index af3b409..66aa778 100644
--- a/browser-extension/src/entrypoints/popup/index.html
+++ b/browser-extension/src/entrypoints/popup/index.html
@@ -8,6 +8,6 @@
-
+