Skip to content

Commit 9b299c3

Browse files
authored
react+tailwind in popup (with testing and playground) (#36)
2 parents 4df7b1b + da33cfa commit 9b299c3

36 files changed

+2477
-247
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ dist/
1818
Thumbs.db
1919

2020
# playright
21+
.playwright-mcp/
22+
browser-extension/dist-playground/
2123
browser-extension/playwright-report/
2224
browser-extension/playwright/
23-
browser-extension/test-results/
25+
browser-extension/test-results/

browser-extension/README.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ This is a [WXT](https://wxt.dev/)-based browser extension that
3333

3434
### Entry points
3535

36-
- `src/entrypoints/content.ts` - injected into every webpage
37-
- `src/entrypoints/background.ts` - service worker that manages state and handles messages
38-
- `src/entrypoints/popup` - html/css/ts which opens when the extension's button gets clicked
36+
- [`src/entrypoints/content.ts`](src/entrypoints/content.ts) - injected into every webpage
37+
- [`src/entrypoints/background.ts`](src/entrypoints/background.ts) - service worker that manages state and handles messages
38+
- [`src/entrypoints/popup/popup.tsx`](src/entrypoints/popup/popup.tsx) - popup (html/css/tsx) with shadcn/ui table components
3939

4040
```mermaid
4141
graph TD
4242
Content[Content Script<br/>content.ts]
4343
Background[Background Script<br/>background.ts]
44-
Popup[Popup Script<br/>popup/main.ts]
44+
Popup[Popup Script<br/>popup/popup.tsx]
4545
4646
Content -->|ENHANCED/DESTROYED<br/>CommentEvent| Background
4747
Popup -->|GET_OPEN_SPOTS<br/>SWITCH_TO_TAB| Background
@@ -60,22 +60,20 @@ graph TD
6060
class TextArea,UI ui
6161
```
6262

63-
### Architecture
63+
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.
6464

65-
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`].
65+
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.
6666

67-
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).
68-
69-
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).
67+
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.
7068

7169
## Testing
7270

73-
In `tests/har` there are various `.har` files. These are complete recordings of a single page load.
74-
75-
- `pnpm run har:view` and you can see the recordings, with or without our browser extension.
71+
- `npm run playground` gives you a test environment where you can tinker with the popup with various test data, supports hot reload
72+
- `npm run har:view` gives you recordings of various web pages which you can see with and without enhancement by the browser extension
7673

7774
### Recording new HAR files
7875

76+
- the har recordings live in `tests/har`, they are complete recordings of the network requests of a single page load
7977
- you can add or change URLs in `tests/har-index.ts`
8078
- `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens
8179
- login manually, then close the browser

browser-extension/biome.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
"noUnusedVariables": "error",
4848
"useValidTypeof": "error"
4949
},
50+
"nursery": {
51+
"useSortedClasses": "error"
52+
},
5053
"recommended": true,
5154
"style": {
5255
"noDefaultExport": "off",
@@ -60,12 +63,14 @@
6063
"useTemplate": "error"
6164
},
6265
"suspicious": {
66+
"noAssignInExpressions": "off",
6367
"noConsole": {
6468
"options": {
6569
"allow": ["assert", "error", "info", "warn"]
6670
}
6771
},
6872
"noExplicitAny": "off",
73+
"noUnknownAtRules": "off",
6974
"noVar": "error"
7075
}
7176
}

browser-extension/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
{
22
"author": "DiffPlug",
33
"dependencies": {
4+
"@primer/octicons-react": "^19.18.0",
5+
"@types/react": "^19.1.12",
6+
"@types/react-dom": "^19.1.9",
47
"@wxt-dev/webextension-polyfill": "^1.0.0",
58
"highlight.js": "^11.11.1",
9+
"lucide-react": "^0.543.0",
610
"overtype": "workspace:*",
11+
"react": "^19.1.1",
12+
"react-dom": "^19.1.1",
13+
"tailwind-merge": "^3.3.1",
14+
"tailwind-variants": "^3.1.1",
715
"webextension-polyfill": "^0.12.0"
816
},
917
"description": "Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).",
1018
"devDependencies": {
1119
"@biomejs/biome": "^2.1.2",
1220
"@playwright/test": "^1.46.0",
21+
"@tailwindcss/vite": "^4.1.13",
1322
"@testing-library/jest-dom": "^6.6.4",
1423
"@types/express": "^4.17.21",
1524
"@types/har-format": "^1.2.16",
1625
"@types/node": "^22.16.5",
26+
"@vitejs/plugin-react": "^5.0.2",
1727
"@vitest/coverage-v8": "^3.2.4",
1828
"@vitest/ui": "^3.2.4",
1929
"express": "^4.19.2",
2030
"linkedom": "^0.18.12",
31+
"postcss": "^8.5.6",
32+
"tailwindcss": "^4.1.13",
2133
"tsx": "^4.19.1",
2234
"typescript": "^5.8.3",
35+
"vite": "^7.1.5",
2336
"vitest": "^3.2.4",
2437
"wxt": "^0.20.7"
2538
},
@@ -47,6 +60,8 @@
4760
"dev:firefox": "wxt -b firefox",
4861
"postinstall": "wxt prepare",
4962
"test": "vitest run",
63+
"playground": "vite --config vite.playground.config.ts",
64+
"playground:build": "vite build --config vite.playground.config.ts",
5065
"har:record": "tsx tests/har-record.ts",
5166
"har:view": "tsx tests/har-view.ts"
5267
},
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { twMerge } from 'tailwind-merge'
2+
import type { VariantProps } from 'tailwind-variants'
3+
import { badgeCVA, typeIcons } from '@/components/design'
4+
5+
export type BadgeProps = VariantProps<typeof badgeCVA> & {
6+
type: keyof typeof typeIcons
7+
text?: number | string
8+
}
9+
10+
const Badge = ({ text, type }: BadgeProps) => {
11+
const Icon = typeIcons[type]
12+
return (
13+
<span
14+
className={twMerge(
15+
badgeCVA({
16+
type,
17+
}),
18+
)}
19+
>
20+
{type === 'blank' || <Icon className='h-3 w-3' />}
21+
{text || type}
22+
</span>
23+
)
24+
}
25+
26+
export default Badge
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { badgeCVA, typeIcons } from '@/components/design'
2+
3+
interface Segment<T> {
4+
text?: string
5+
type: keyof typeof typeIcons
6+
value: T
7+
}
8+
interface MultiSegmentProps<T> {
9+
segments: Segment<T>[]
10+
value: T
11+
onValueChange: (value: T) => void
12+
}
13+
14+
const MultiSegment = <T,>({ segments, value, onValueChange }: MultiSegmentProps<T>) => {
15+
return (
16+
<div className='inline-flex items-center gap-0'>
17+
{segments.map((segment, index) => {
18+
const Icon = typeIcons[segment.type]
19+
const isFirst = index === 0
20+
const isLast = index === segments.length - 1
21+
22+
const roundedClasses =
23+
isFirst && isLast
24+
? ''
25+
: isFirst
26+
? '!rounded-r-none'
27+
: isLast
28+
? '!rounded-l-none'
29+
: '!rounded-none'
30+
31+
return (
32+
<button
33+
key={String(segment.value)}
34+
className={`${badgeCVA({
35+
clickable: true,
36+
selected: value === segment.value,
37+
type: segment.type,
38+
})} ${roundedClasses}`}
39+
onClick={() => onValueChange(segment.value)}
40+
type='button'
41+
>
42+
{segment.type === 'blank' || <Icon className='h-3 w-3' />}
43+
{segment.text}
44+
</button>
45+
)
46+
})}
47+
</div>
48+
)
49+
}
50+
51+
export default MultiSegment

0 commit comments

Comments
 (0)