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 33cd57f..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", @@ -60,12 +63,14 @@ "useTemplate": "error" }, "suspicious": { + "noAssignInExpressions": "off", "noConsole": { "options": { "allow": ["assert", "error", "info", "warn"] } }, "noExplicitAny": "off", + "noUnknownAtRules": "off", "noVar": "error" } } diff --git a/browser-extension/package.json b/browser-extension/package.json index 128aa79..9097cba 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,25 +1,38 @@ { "author": "DiffPlug", "dependencies": { + "@primer/octicons-react": "^19.18.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", "@wxt-dev/webextension-polyfill": "^1.0.0", "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).", "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 +60,8 @@ "dev:firefox": "wxt -b firefox", "postinstall": "wxt prepare", "test": "vitest run", + "playground": "vite --config vite.playground.config.ts", + "playground:build": "vite build --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/Badge.tsx b/browser-extension/src/components/Badge.tsx new file mode 100644 index 0000000..2f64a53 --- /dev/null +++ b/browser-extension/src/components/Badge.tsx @@ -0,0 +1,26 @@ +import { twMerge } from 'tailwind-merge' +import type { VariantProps } from 'tailwind-variants' +import { badgeCVA, 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/src/components/MultiSegment.tsx b/browser-extension/src/components/MultiSegment.tsx new file mode 100644 index 0000000..62cc078 --- /dev/null +++ b/browser-extension/src/components/MultiSegment.tsx @@ -0,0 +1,51 @@ +import { badgeCVA, typeIcons } from '@/components/design' + +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 ( + + ) + })} +
+ ) +} + +export default MultiSegment diff --git a/browser-extension/src/components/PopupRoot.tsx b/browser-extension/src/components/PopupRoot.tsx new file mode 100644 index 0000000..ec32e04 --- /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='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' + /> +
+
+ + + 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/design.tsx b/browser-extension/src/components/design.tsx new file mode 100644 index 0000000..e99ef68 --- /dev/null +++ b/browser-extension/src/components/design.tsx @@ -0,0 +1,62 @@ +import { + Clock, + Code, + EyeOff, + Image, + Link, + MailCheck, + MessageSquareDashed, + Monitor, + Settings, + TextSelect, + Trash2, +} from 'lucide-react' +import { tv } from 'tailwind-variants' + +// 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', + }, + 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/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/src/entrypoints/background.ts b/browser-extension/src/entrypoints/background.ts index 3e071a1..6a8659b 100644 --- a/browser-extension/src/entrypoints/background.ts +++ b/browser-extension/src/entrypoints/background.ts @@ -1,24 +1,39 @@ -import type { CommentDraft, CommentEvent, CommentSpot } from '../lib/enhancer' -import type { GetOpenSpotsResponse, ToBackgroundMessage } from '../lib/messages' +import type { CommentEvent, CommentSpot } from '@/lib/enhancer' +import { type DraftStats, statsFor } from '@/lib/enhancers/draftStats' +import type { GetTableRowsResponse, 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 windowId: number } -export interface CommentState { +export interface CommentStorage { tab: Tab spot: CommentSpot - drafts: [number, CommentDraft][] + drafts: [number, string][] + sentOn: number | null + trashedOn: number | null +} +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() +export const openSpots = new Map() export function handleCommentEvent(message: CommentEvent, sender: any): boolean { if ( @@ -27,14 +42,15 @@ 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 = { + const commentState: CommentStorage = { drafts: [], + sentOn: null, spot: message.spot, - tab, + tab: { + tabId: sender.tab.id, + windowId: sender.tab.windowId, + }, + trashedOn: null, } openSpots.set(message.spot.unique_key, commentState) } else if (message.type === 'DESTROYED') { @@ -52,11 +68,22 @@ 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 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/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 @@
- + \ 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/popup.tsx b/browser-extension/src/entrypoints/popup/popup.tsx new file mode 100644 index 0000000..7788059 --- /dev/null +++ b/browser-extension/src/entrypoints/popup/popup.tsx @@ -0,0 +1,57 @@ +import './style.css' +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 { + const message: GetOpenSpotsMessage = { type: 'GET_OPEN_SPOTS' } + const response = (await browser.runtime.sendMessage(message)) as GetTableRowsResponse + logger.debug('Received response:', response) + 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 handleSpotClick = (spot: CommentTableRow) => { +// console.log('TODO: switchToTab') +// //switchToTab(spot.tab.tabId, spot.tab.windowId) +// } + +const app = document.getElementById('app') +if (app) { + const root = createRoot(app) + + // 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/src/entrypoints/popup/style.css b/browser-extension/src/entrypoints/popup/style.css index 0d63390..d718f6e 100644 --- a/browser-extension/src/entrypoints/popup/style.css +++ b/browser-extension/src/entrypoints/popup/style.css @@ -1,53 +1,17 @@ +@import "tailwindcss"; +@source "../../.."; +/* Popup window frame styles */ +:root { + --popup-width: 600px; + --popup-height: 400px; +} + body { - width: 300px; + 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; } - -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..5c9e95b 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: @@ -10,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. */ @@ -40,7 +36,8 @@ export interface CommentEnhancer { * exactly once since pageload before this gets called. */ enhance(textarea: HTMLTextAreaElement, spot: Spot): OverTypeInstance - - tableIcon(spot: Spot): string + /** Returns a ReactNode which will be displayed in the table row. */ + tableUpperDecoration(spot: Spot): ReactNode + /** The default title of a row */ tableTitle(spot: Spot): string } diff --git a/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx b/browser-extension/src/lib/enhancers/CommentEnhancerMissing.tsx new file mode 100644 index 0000000..a470feb --- /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/enhancers/draftStats.ts b/browser-extension/src/lib/enhancers/draftStats.ts new file mode 100644 index 0000000..0ffc0ea --- /dev/null +++ b/browser-extension/src/lib/enhancers/draftStats.ts @@ -0,0 +1,62 @@ +export interface MdImage { + url: string + alt?: string +} + +export interface MdLink { + text: string + url: string +} + +export interface MdCodeBlock { + language?: string + code: string +} + +export interface DraftStats { + charCount: number + images: MdImage[] + links: MdLink[] + codeBlocks: MdCodeBlock[] +} +export function statsFor(md: string): DraftStats { + const charCount = md.length + + const images: MdImage[] = [] + const links: MdLink[] = [] + const codeBlocks: MdCodeBlock[] = [] + + const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g + let imageMatch: RegExpExecArray | null + while ((imageMatch = imageRegex.exec(md)) !== null) { + images.push({ + ...(imageMatch[1] && { alt: imageMatch[1] }), + url: imageMatch[2]!, + }) + } + + const linkRegex = /(? + + + + #{spot.number} + + {spot.slug} + + + ) } - tableIcon(_: GitHubIssueAddCommentSpot): string { - return '๐Ÿ”„' // PR icon TODO: icon urls in /public - } - - buildUrl(spot: GitHubIssueAddCommentSpot): string { - return `https://${spot.domain}/${spot.slug}/issue/${spot.number}` + tableTitle(_spot: GitHubIssueAddCommentSpot): string { + return 'TITLE_TODO' } } diff --git a/browser-extension/src/lib/enhancers/github/githubIssueNewComment.ts b/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx similarity index 86% rename from browser-extension/src/lib/enhancers/github/githubIssueNewComment.ts rename to browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx index b2ad4b6..6d26515 100644 --- a/browser-extension/src/lib/enhancers/github/githubIssueNewComment.ts +++ b/browser-extension/src/lib/enhancers/github/githubIssueNewComment.tsx @@ -52,13 +52,18 @@ export class GitHubIssueNewCommentEnhancer implements CommentEnhancer + New Issue + {slug} + + ) } - tableIcon(_: GitHubIssueNewCommentSpot): string { - return '๐Ÿ”„' // PR icon TODO: icon urls in /public + tableTitle(_spot: GitHubIssueNewCommentSpot): string { + return 'New Issue' } buildUrl(spot: GitHubIssueNewCommentSpot): string { diff --git a/browser-extension/src/lib/enhancers/github/githubPRAddComment.ts b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx similarity index 77% rename from browser-extension/src/lib/enhancers/github/githubPRAddComment.ts rename to browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx index e1b2260..b223339 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRAddComment.ts +++ b/browser-extension/src/lib/enhancers/github/githubPRAddComment.tsx @@ -1,12 +1,14 @@ import OverType, { type OverTypeInstance } from 'overtype' -import type { CommentEnhancer, CommentSpot } from '../../enhancer' -import { logger } from '../../logger' +import type React from 'react' +import type { CommentEnhancer, CommentSpot } from '@/lib/enhancer' +import { logger } from '@/lib/logger' import { modifyDOM } from '../modifyDOM' import { commonGithubOptions } from './ghOptions' 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 + title: string domain: string slug: string // owner/repo number: number // issue/PR number, undefined for new issues and PRs @@ -36,10 +38,12 @@ export class GitHubPRAddCommentEnhancer implements CommentEnhancer + {slug} + PR #{number} + + ) } - tableIcon(_: GitHubPRAddCommentSpot): string { - return '๐Ÿ”„' // PR icon TODO: icon urls in /public - } - - buildUrl(spot: GitHubPRAddCommentSpot): string { - return `https://${spot.domain}/${spot.slug}/pull/${spot.number}` + tableTitle(_spot: GitHubPRAddCommentSpot): string { + return 'TITLE_TODO' } } diff --git a/browser-extension/src/lib/enhancers/github/githubPRNewComment.ts b/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx similarity index 88% rename from browser-extension/src/lib/enhancers/github/githubPRNewComment.ts rename to browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx index bd9d6ac..4996f6d 100644 --- a/browser-extension/src/lib/enhancers/github/githubPRNewComment.ts +++ b/browser-extension/src/lib/enhancers/github/githubPRNewComment.tsx @@ -57,13 +57,18 @@ export class GitHubPRNewCommentEnhancer implements CommentEnhancer + New PR + {slug} + + ) } - tableIcon(_: GitHubPRNewCommentSpot): string { - return '๐Ÿ”„' // PR icon TODO: icon urls in /public + tableTitle(_spot: GitHubPRNewCommentSpot): string { + return 'TITLE_TODO' } buildUrl(spot: GitHubPRNewCommentSpot): string { diff --git a/browser-extension/src/lib/messages.ts b/browser-extension/src/lib/messages.ts index e04e8c5..09d0298 100644 --- a/browser-extension/src/lib/messages.ts +++ b/browser-extension/src/lib/messages.ts @@ -1,4 +1,5 @@ -import type { CommentDraft, 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, CommentDraft]> - }> +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/src/lib/registries.ts b/browser-extension/src/lib/registries.ts index cce6ce9..3482dcc 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 { 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/lib/enhancers/draftStats.test.ts b/browser-extension/tests/lib/enhancers/draftStats.test.ts new file mode 100644 index 0000000..cc230c7 --- /dev/null +++ b/browser-extension/tests/lib/enhancers/draftStats.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it } from 'vitest' +import { statsFor } from '../../../src/lib/enhancers/draftStats' + +describe('statsFor', () => { + it('should handle empty markdown', () => { + expect(statsFor('')).toMatchInlineSnapshot(` + { + "charCount": 0, + "codeBlocks": [], + "images": [], + "links": [], + } + `) + }) + + it('should count characters', () => { + expect(statsFor('Hello world')).toMatchInlineSnapshot(` + { + "charCount": 11, + "codeBlocks": [], + "images": [], + "links": [], + } + `) + }) + + it('should extract images with alt text', () => { + expect(statsFor('![Alt text](https://example.com/image.png)')).toMatchInlineSnapshot(` + { + "charCount": 42, + "codeBlocks": [], + "images": [ + { + "alt": "Alt text", + "url": "https://example.com/image.png", + }, + ], + "links": [], + } + `) + }) + + it('should extract images without alt text', () => { + expect(statsFor('![](https://example.com/image.png)')).toMatchInlineSnapshot(` + { + "charCount": 34, + "codeBlocks": [], + "images": [ + { + "url": "https://example.com/image.png", + }, + ], + "links": [], + } + `) + }) + + it('should extract links', () => { + expect(statsFor('[Link text](https://example.com)')).toMatchInlineSnapshot(` + { + "charCount": 32, + "codeBlocks": [], + "images": [], + "links": [ + { + "text": "Link text", + "url": "https://example.com", + }, + ], + } + `) + }) + + it('should extract code blocks with language', () => { + expect(statsFor('```javascript\nconsole.log("hello")\n```')).toMatchInlineSnapshot(` + { + "charCount": 38, + "codeBlocks": [ + { + "code": "console.log("hello") + ", + "language": "javascript", + }, + ], + "images": [], + "links": [], + } + `) + }) + + it('should extract code blocks without language', () => { + expect(statsFor('```\nconsole.log("hello")\n```')).toMatchInlineSnapshot(` + { + "charCount": 28, + "codeBlocks": [ + { + "code": "console.log("hello") + ", + }, + ], + "images": [], + "links": [], + } + `) + }) + + it('should handle complex markdown with multiple elements', () => { + const markdown = `# Title + +Here's some text with a [link](https://example.com) and an ![image](https://example.com/img.png). + +\`\`\`typescript +function hello() { + return "world" +} +\`\`\` + +More text with another ![alt text](https://example.com/img2.jpg) and [another link](https://test.com). + +\`\`\` +plain code block +\`\`\`` + + expect(statsFor(markdown)).toMatchInlineSnapshot(` + { + "charCount": 293, + "codeBlocks": [ + { + "code": "function hello() { + return "world" + } + ", + "language": "typescript", + }, + { + "code": "plain code block + ", + }, + ], + "images": [ + { + "alt": "image", + "url": "https://example.com/img.png", + }, + { + "alt": "alt text", + "url": "https://example.com/img2.jpg", + }, + ], + "links": [ + { + "text": "link", + "url": "https://example.com", + }, + { + "text": "another link", + "url": "https://test.com", + }, + ], + } + `) + }) +}) diff --git a/browser-extension/tests/lib/enhancers/github.test.ts b/browser-extension/tests/lib/enhancers/github.test.ts index 1b56dea..efdba52 100644 --- a/browser-extension/tests/lib/enhancers/github.test.ts +++ b/browser-extension/tests/lib/enhancers/github.test.ts @@ -11,15 +11,34 @@ 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, "slug": "diffplug/selfie", + "title": "TODO_TITLE", "type": "GH_PR_ADD_COMMENT", "unique_key": "github.com:diffplug/selfie:517", } `) + expect( + enhancedTextarea?.enhancer.tableUpperDecoration(enhancedTextarea.spot), + ).toMatchInlineSnapshot(` + + + diffplug/selfie + + + PR # + 517 + + + `) }) usingHar('gh_new_pr').it('should create the correct spot object', async () => { const enhancers = new EnhancerRegistry() @@ -38,15 +57,39 @@ describe('github', () => { 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, "slug": "diffplug/selfie", + "title": "TODO_TITLE", "type": "GH_ISSUE_ADD_COMMENT", "unique_key": "github.com:diffplug/selfie:523", } `) + // Test the tableRow method + expect( + enhancedTextarea?.enhancer.tableUpperDecoration(enhancedTextarea.spot), + ).toMatchInlineSnapshot(` + + + + + # + 523 + + diffplug/selfie + + + `) }) usingHar('gh_new_issue').it('should create the correct spot object', async () => { const enhancers = new EnhancerRegistry() diff --git a/browser-extension/tests/playground/claude.tsx b/browser-extension/tests/playground/claude.tsx new file mode 100644 index 0000000..8e8a15b --- /dev/null +++ b/browser-extension/tests/playground/claude.tsx @@ -0,0 +1,306 @@ +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' +import { generateMockDrafts } from './replicaData' + +export const ClaudePrototype = () => { + const [drafts] = useState(generateMockDrafts()) + const [selectedIds, setSelectedIds] = useState(new Set()) + const [filters, setFilters] = useState({ + searchQuery: '', + sentFilter: 'both', + showTrashed: false, + }) + + 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='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' + /> +
+
+ + + 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/tests/playground/index.html b/browser-extension/tests/playground/index.html new file mode 100644 index 0000000..0138cf2 --- /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/playground-styles.css b/browser-extension/tests/playground/playground-styles.css new file mode 100644 index 0000000..e6587c5 --- /dev/null +++ b/browser-extension/tests/playground/playground-styles.css @@ -0,0 +1,28 @@ +/* Playground-specific styles - popup styles are imported via popup/style.css */ + +/* Override body styles for playground layout */ +body { + margin: 0; + padding: 2rem; + background: #f8fafc; + min-height: 100vh; + width: auto; /* Override popup's fixed width for playground */ +} + +#root { + max-width: 1200px; + margin: 0; +} + +/* Popup simulator frame */ +.popup-frame { + width: var(--popup-width); + height: var(--popup-height); + font-size: 14px; + line-height: 1.4; + background: white; + border: 1px solid #e2e8f0; + border-radius: 0px; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); + margin: 0 auto; +} diff --git a/browser-extension/tests/playground/playground.tsx b/browser-extension/tests/playground/playground.tsx new file mode 100644 index 0000000..fc32087 --- /dev/null +++ b/browser-extension/tests/playground/playground.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react' +import { createRoot } from 'react-dom/client' +import '@/entrypoints/popup/style.css' +import './playground-styles.css' +import { ClaudePrototype } from './claude' +import { Replica } from './replica' + +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('claude') + + return ( +
+
+
+

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]) => ( + + ))} +
+
+ +
+ {(() => { + const Component = MODES[activeComponent].component + return + })()} +
+
+
+ ) +} + +const root = createRoot(document.getElementById('root')!) +root.render() diff --git a/browser-extension/tests/playground/replica.tsx b/browser-extension/tests/playground/replica.tsx new file mode 100644 index 0000000..71c1dfe --- /dev/null +++ b/browser-extension/tests/playground/replica.tsx @@ -0,0 +1,63 @@ +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' + +const gh_pr: GitHubPRAddCommentSpot = { + domain: 'github.com', + number: 517, + slug: 'diffplug/selfie', + title: 'wowza', + type: 'GH_PR_ADD_COMMENT', + unique_key: 'github.com:diffplug/selfie:517', +} +const gh_issue: GitHubIssueAddCommentSpot = { + domain: 'github.com', + number: 523, + slug: 'diffplug/selfie', + title: 'whoa', + type: 'GH_ISSUE_ADD_COMMENT', + unique_key: 'github.com:diffplug/selfie:523', +} + +const spots: CommentSpot[] = [gh_pr, gh_issue] +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() { + return ( + { + 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 + })} + > + ) +} diff --git a/browser-extension/tests/playground/replicaData.tsx b/browser-extension/tests/playground/replicaData.tsx new file mode 100644 index 0000000..7ee7f5f --- /dev/null +++ b/browser-extension/tests/playground/replicaData.tsx @@ -0,0 +1,177 @@ +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' + +export interface RedditSpot extends CommentSpot { + title: string + subreddit: string + type: 'REDDIT' +} + +const withSpot = (spot: T): T => spot + +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: 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), + }, + { + 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: withSpot({ + 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: 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), + }, + { + 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: withSpot({ + 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: 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), + }, +] 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/vite.playground.config.ts b/browser-extension/vite.playground.config.ts new file mode 100644 index 0000000..73ff163 --- /dev/null +++ b/browser-extension/vite.playground.config.ts @@ -0,0 +1,23 @@ +import path from 'node:path' +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + emptyOutDir: true, + outDir: '../../dist-playground', + }, + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve('./src'), + }, + }, + root: 'tests/playground', + server: { + host: true, + open: true, + port: 3002, + }, +}) diff --git a/browser-extension/wxt.config.ts b/browser-extension/wxt.config.ts index 7a4699a..bd70a04 100644 --- a/browser-extension/wxt.config.ts +++ b/browser-extension/wxt.config.ts @@ -1,3 +1,6 @@ +import path from 'node:path' +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' import { defineConfig } from 'wxt' export default defineConfig({ @@ -16,6 +19,14 @@ export default defineConfig({ }, modules: ['@wxt-dev/webextension-polyfill'], srcDir: 'src', + vite: () => ({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve('./src'), + }, + }, + }), webExt: { disabled: true, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8aa1ce..f05be97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,15 +10,39 @@ importers: browser-extension: dependencies: + '@primer/octicons-react': + specifier: ^19.18.0 + version: 19.18.0(react@19.1.1) + '@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)) 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 + 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 @@ -29,6 +53,9 @@ importers: '@playwright/test': specifier: ^1.46.0 version: 1.55.0 + '@tailwindcss/vite': + specifier: ^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 @@ -41,6 +68,9 @@ 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) @@ -53,18 +83,27 @@ 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 tsx: specifier: ^4.19.1 version: 4.20.5 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)(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: @@ -117,6 +156,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 +198,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 +643,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 +703,15 @@ 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' + + '@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 +817,112 @@ 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/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==} 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 +977,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 +994,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: @@ -953,6 +1183,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 +1237,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 +1260,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'} @@ -1120,6 +1362,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 +1400,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 +1496,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 +1545,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 +1568,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'} @@ -1500,6 +1759,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 +2109,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 +2154,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 +2276,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 +2392,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 +2444,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'} @@ -2366,6 +2718,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 +2830,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 +3038,30 @@ 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==} + + 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==} + + 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 +3204,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 +3434,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'} @@ -3101,16 +3510,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 +3853,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 +3911,12 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@primer/octicons-react@19.18.0(react@19.1.1)': + dependencies: + react: 19.1.1 + + '@rolldown/pluginutils@1.0.0-beta.34': {} + '@rollup/rollup-android-arm-eabi@4.50.1': optional: true @@ -3468,6 +3980,77 @@ 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/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: + '@tailwindcss/node': 4.1.13 + '@tailwindcss/oxide': 4.1.13 + 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: '@adobe/css-tools': 4.4.4 @@ -3477,6 +4060,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 +4138,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 +4162,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 +4189,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 +4201,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 +4238,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 +4267,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: @@ -3769,6 +4393,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 +4455,8 @@ snapshots: camelcase@8.0.0: {} + caniuse-lite@1.0.30001741: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3845,6 +4478,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@3.0.0: {} + chrome-launcher@1.2.0: dependencies: '@types/node': 22.18.1 @@ -3947,6 +4582,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.0.6: {} cookie@0.7.1: {} @@ -3980,6 +4617,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 +4678,8 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.0.4: {} + dom-accessibility-api@0.6.3: {} dom-serializer@2.0.0: @@ -4083,6 +4724,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 +4740,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: {} @@ -4337,6 +4985,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 +5328,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 +5370,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 +5476,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 +5580,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 +5631,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 @@ -5232,6 +5945,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 +6074,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 +6294,27 @@ snapshots: symbol-tree@3.2.4: {} + 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: {} + + 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 +6445,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 +6479,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 +6500,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 +6512,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 +6537,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 +6667,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 +6711,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 +6745,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: {}