a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: revision graph improvements with multi-select and better branch ordering

- Add multi-select support with checkboxes for revision graph
- Improve collapsed stack UI with stacked card animation
- Add ast-grep rules (migrate from .grit files)
- Add no-direct-tauri-mutations lint rule
- Add livestore Tauri adapter and sync infrastructure
- Refactor db.ts to use collections with prefetching
- Add checkbox UI component
- Improve browser-only mocks for dev mode
- Various UI polish and cleanup

+4039 -510
-5
apps/desktop/biome.jsonc
··· 1 1 { 2 2 "$schema": "node_modules/@biomejs/biome/configuration_schema.json", 3 - "plugins": [ 4 - "./no-react-memoization.grit", 5 - "./no-usestate.grit", 6 - "./no-useeffect-data-fetching.grit" 7 - ], 8 3 "vcs": { 9 4 "enabled": false, 10 5 "clientKind": "git",
-12
apps/desktop/no-react-memoization.grit
··· 1 - `$fn($args)` where { 2 - or { 3 - $fn <: `useCallback`, 4 - $fn <: `useMemo`, 5 - $fn <: `memo` 6 - }, 7 - register_diagnostic( 8 - span = $fn, 9 - message = "Manual memoization (useCallback/useMemo/memo) is unnecessary with React Compiler. Use regular functions/components instead.", 10 - severity = "warn" 11 - ) 12 - }
-9
apps/desktop/no-useeffect-data-fetching.grit
··· 1 - `$fn($args)` where { 2 - $fn <: `useEffect`, 3 - register_diagnostic( 4 - span = $fn, 5 - message = "useEffect for data fetching is discouraged. Use TanStack DB (useLiveQuery) instead.", 6 - severity = "warn" 7 - ) 8 - } 9 -
-9
apps/desktop/no-usestate.grit
··· 1 - `$fn($args)` where { 2 - $fn <: `useState`, 3 - register_diagnostic( 4 - span = $fn, 5 - message = "useState is discouraged. Use atoms from @/atoms.ts instead for global state management.", 6 - severity = "warn" 7 - ) 8 - } 9 -
+59 -59
apps/desktop/package.json
··· 1 1 { 2 - "name": "desktop", 3 - "version": "0.1.0", 4 - "private": true, 5 - "type": "module", 6 - "scripts": { 7 - "dev": "vite", 8 - "build": "tsgo && vite build", 9 - "preview": "vite preview", 10 - "tauri": "tauri", 11 - "typecheck": "tsgo --noEmit", 12 - "lint": "biome check .", 13 - "format": "biome format --write ." 14 - }, 15 - "dependencies": { 16 - "@base-ui/react": "^1.0.0", 17 - "@effect-atom/atom": "^0.4.11", 18 - "@effect-atom/atom-react": "^0.4.4", 19 - "@fontsource-variable/jetbrains-mono": "^5.2.8", 20 - "@pierre/diffs": "^1.0.4", 21 - "@tailwindcss/vite": "^4.1.18", 22 - "@tanstack/db": "^0.5.15", 23 - "@tanstack/query-core": "^5.90.12", 24 - "@tanstack/query-db-collection": "^1.0.11", 25 - "@tanstack/react-db": "^0.1.59", 26 - "@tanstack/react-query": "^5.90.12", 27 - "@tanstack/react-router": "^1.141.6", 28 - "@tanstack/react-virtual": "^3.13.16", 29 - "@tauri-apps/api": "^2.1.1", 30 - "@tauri-apps/plugin-deep-link": "~2", 31 - "@tauri-apps/plugin-dialog": "^2.4.2", 32 - "@tauri-apps/plugin-shell": "^2.0.1", 33 - "@tauri-apps/plugin-sql": "^2.3.1", 34 - "@tauri-apps/plugin-store": "^2.4.1", 35 - "class-variance-authority": "^0.7.1", 36 - "clsx": "^2.1.1", 37 - "cmdk": "^1.1.1", 38 - "effect": "^3.19.13", 39 - "lucide-react": "^0.562.0", 40 - "react": "19", 41 - "react-dom": "19", 42 - "react-resizable-panels": "^4.0.10", 43 - "scheduler": "^0.27.0", 44 - "shadcn": "^3.6.2", 45 - "tailwind-merge": "^3.4.0", 46 - "tw-animate-css": "^1.4.0" 47 - }, 48 - "devDependencies": { 49 - "@biomejs/biome": "^2.3.10", 50 - "@tauri-apps/cli": "^2.1.0", 51 - "@types/node": "^25.0.3", 52 - "@types/react": "19", 53 - "@types/react-dom": "19", 54 - "@typescript/native-preview": "^7.0.0-dev.20251219.1", 55 - "@vitejs/plugin-react": "^4.3.4", 56 - "babel-plugin-react-compiler": "^1.0.0", 57 - "tailwindcss": "^4.1.18", 58 - "typescript": "^5.6.3", 59 - "vite": "^5.4.11" 60 - } 2 + "name": "desktop", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsgo && vite build", 9 + "preview": "vite preview", 10 + "tauri": "tauri", 11 + "typecheck": "tsgo --noEmit", 12 + "lint": "biome check . && ast-grep scan", 13 + "format": "biome format --write ." 14 + }, 15 + "dependencies": { 16 + "@base-ui/react": "^1.0.0", 17 + "@effect-atom/atom": "^0.4.11", 18 + "@effect-atom/atom-react": "^0.4.4", 19 + "@fontsource-variable/jetbrains-mono": "^5.2.8", 20 + "@pierre/diffs": "^1.0.4", 21 + "@tailwindcss/vite": "^4.1.18", 22 + "@tanstack/db": "^0.5.15", 23 + "@tanstack/query-core": "^5.90.12", 24 + "@tanstack/query-db-collection": "^1.0.11", 25 + "@tanstack/react-db": "^0.1.59", 26 + "@tanstack/react-query": "^5.90.12", 27 + "@tanstack/react-router": "^1.141.6", 28 + "@tanstack/react-virtual": "^3.13.16", 29 + "@tauri-apps/api": "^2.1.1", 30 + "@tauri-apps/plugin-deep-link": "~2", 31 + "@tauri-apps/plugin-dialog": "^2.4.2", 32 + "@tauri-apps/plugin-shell": "^2.0.1", 33 + "@tauri-apps/plugin-sql": "^2.3.1", 34 + "@tauri-apps/plugin-store": "^2.4.1", 35 + "class-variance-authority": "^0.7.1", 36 + "clsx": "^2.1.1", 37 + "cmdk": "^1.1.1", 38 + "effect": "^3.19.13", 39 + "lucide-react": "^0.562.0", 40 + "react": "19", 41 + "react-dom": "19", 42 + "react-resizable-panels": "^4.0.10", 43 + "scheduler": "^0.27.0", 44 + "shadcn": "^3.6.2", 45 + "tailwind-merge": "^3.4.0", 46 + "tw-animate-css": "^1.4.0" 47 + }, 48 + "devDependencies": { 49 + "@biomejs/biome": "^2.3.10", 50 + "@tauri-apps/cli": "^2.1.0", 51 + "@types/node": "^25.0.3", 52 + "@types/react": "19", 53 + "@types/react-dom": "19", 54 + "@typescript/native-preview": "^7.0.0-dev.20251219.1", 55 + "@vitejs/plugin-react": "^4.3.4", 56 + "babel-plugin-react-compiler": "^1.0.0", 57 + "tailwindcss": "^4.1.18", 58 + "typescript": "^5.6.3", 59 + "vite": "^5.4.11" 60 + } 61 61 }
+33
apps/desktop/rules/no-direct-tauri-mutations.yml
··· 1 + # Rule: Tauri mutation commands should only be called from db.ts 2 + # These functions update backend state and must go through TanStack DB collections 3 + # to maintain the source of truth pattern. 4 + 5 + id: no-direct-tauri-mutations 6 + language: tsx 7 + severity: error 8 + message: "Direct import of mutation function from tauri-commands is restricted. Use TanStack DB mutation functions from '@/db' instead." 9 + note: | 10 + Restricted functions and their replacements: 11 + - upsertRepository → addRepository() or updateRepository() from db.ts 12 + - removeRepository → deleteRepository() from db.ts 13 + - jjNew → newRevision() from db.ts 14 + - jjEdit → editRevision() from db.ts 15 + - jjAbandon → abandonRevision() from db.ts 16 + - watchRepository/unwatchRepository → handled internally by collections 17 + 18 + files: 19 + - "src/**/*.ts" 20 + - "src/**/*.tsx" 21 + - "!src/db.ts" 22 + - "!src/tauri-commands.ts" 23 + 24 + rule: 25 + all: 26 + - kind: import_specifier 27 + - regex: "^(upsertRepository|removeRepository|jjNew|jjEdit|jjAbandon|watchRepository|unwatchRepository)$" 28 + - inside: 29 + stopBy: end 30 + kind: import_statement 31 + has: 32 + kind: string 33 + regex: "tauri-commands"
+10
apps/desktop/rules/no-react-memoization.yml
··· 1 + # Rule: Discourage manual memoization with React Compiler 2 + id: no-react-memoization 3 + language: typescript 4 + severity: warning 5 + message: "Manual memoization is unnecessary with React Compiler. Use regular functions/components instead." 6 + rule: 7 + any: 8 + - pattern: useCallback($$$ARGS) 9 + - pattern: useMemo($$$ARGS) 10 + - pattern: memo($$$ARGS)
+7
apps/desktop/rules/no-useeffect-data-fetching.yml
··· 1 + # Rule: Discourage useEffect for data fetching 2 + id: no-useeffect-data-fetching 3 + language: typescript 4 + severity: warning 5 + message: "useEffect for data fetching is discouraged. Use TanStack DB (useLiveQuery) instead." 6 + rule: 7 + pattern: useEffect($$$ARGS)
+7
apps/desktop/rules/no-usestate.yml
··· 1 + # Rule: Discourage useState in favor of atoms 2 + id: no-usestate 3 + language: typescript 4 + severity: warning 5 + message: "useState is discouraged. Use atoms from '@/atoms.ts' instead for global state management." 6 + rule: 7 + pattern: useState($$$ARGS)
+5
apps/desktop/sgconfig.yml
··· 1 + # ast-grep configuration 2 + # https://ast-grep.github.io/reference/sgconfig.html 3 + 4 + ruleDirs: 5 + - rules
+30 -22
apps/desktop/src/components/AceJump.tsx
··· 22 22 // Highlight matching text in a string 23 23 function HighlightMatch({ text, query }: { text: string; query: string }): React.ReactElement { 24 24 if (!query) return <>{text}</>; 25 - 25 + 26 26 const lowerText = text.toLowerCase(); 27 27 const lowerQuery = query.toLowerCase(); 28 28 const index = lowerText.indexOf(lowerQuery); 29 - 29 + 30 30 if (index === -1) return <>{text}</>; 31 - 31 + 32 32 const before = text.slice(0, index); 33 33 const match = text.slice(index, index + query.length); 34 34 const after = text.slice(index + query.length); 35 - 35 + 36 36 return ( 37 37 <> 38 38 {before} ··· 46 46 function isRevsetExpression(query: string): boolean { 47 47 const trimmed = query.trim(); 48 48 if (!trimmed) return false; 49 - 49 + 50 50 // Revset patterns: @, @-, @--, id-, id+, or any jj revset syntax 51 51 // We'll be more liberal and consider anything with special chars as potential revset 52 52 if (trimmed === "@") return true; ··· 58 58 if (trimmed.includes("&")) return true; // intersection 59 59 if (trimmed.includes("::")) return true; // ancestors 60 60 if (trimmed.includes("..")) return true; // range 61 - 61 + 62 62 return false; 63 63 } 64 64 ··· 148 148 ); 149 149 150 150 // Determine if we're in revset mode 151 - const isRevsetMode = isRevsetExpression(search) && (revsetResult.loading || revsetResult.changeIds.length > 0 || revsetResult.error); 151 + const isRevsetMode = 152 + isRevsetExpression(search) && 153 + (revsetResult.loading || revsetResult.changeIds.length > 0 || revsetResult.error); 152 154 const revsetChangeIdSet = useMemo( 153 155 () => new Set(revsetResult.changeIds), 154 156 [revsetResult.changeIds], 155 157 ); 156 158 157 159 // Determine what matched for each revision 158 - function getMatchType(revision: Revision): "revset" | "changeId" | "bookmark" | "description" | null { 160 + function getMatchType( 161 + revision: Revision, 162 + ): "revset" | "changeId" | "bookmark" | "description" | null { 159 163 if (isRevsetMode && revsetChangeIdSet.has(revision.change_id)) { 160 164 return "revset"; 161 165 } 162 166 if (!search || isRevsetMode) return null; 163 167 const lowerSearch = search.toLowerCase(); 164 - 168 + 165 169 if (revision.change_id.toLowerCase().startsWith(lowerSearch)) return "changeId"; 166 170 if (revision.bookmarks.some((b) => b.toLowerCase().includes(lowerSearch))) return "bookmark"; 167 171 if (revision.description.toLowerCase().includes(lowerSearch)) return "description"; ··· 177 181 // Custom filter function that ranks by match type 178 182 function customFilter(value: string, searchQuery: string): number { 179 183 if (!searchQuery) return 1; // Show all when no search 180 - 184 + 181 185 const revision = revisionByChangeId.get(value); 182 186 if (!revision) return 0; 183 - 187 + 184 188 // Revset match - highest priority 185 189 if (isRevsetMode) { 186 190 return revsetChangeIdSet.has(value) ? 1.0 : 0; 187 191 } 188 - 192 + 189 193 const lowerSearch = searchQuery.toLowerCase(); 190 - 194 + 191 195 // Change ID match - highest priority 192 196 if (revision.change_id.toLowerCase().startsWith(lowerSearch)) { 193 197 return 1.0; 194 198 } 195 - 199 + 196 200 // Bookmark match - medium priority 197 201 if (revision.bookmarks.some((b) => b.toLowerCase().includes(lowerSearch))) { 198 202 return 0.7; 199 203 } 200 - 204 + 201 205 // Description match - lower priority 202 206 if (revision.description.toLowerCase().includes(lowerSearch)) { 203 207 return 0.4; 204 208 } 205 - 209 + 206 210 return 0; 207 211 } 208 212 ··· 242 246 "No revisions found." 243 247 )} 244 248 </CommandEmpty> 245 - {isRevsetMode && !revsetResult.loading && !revsetResult.error && revsetResult.changeIds.length > 0 && ( 246 - <div className="px-3 py-2 text-xs text-muted-foreground border-b border-border"> 247 - revset: {revsetResult.label} ({revsetResult.changeIds.length} match{revsetResult.changeIds.length !== 1 ? "es" : ""}) 248 - </div> 249 - )} 249 + {isRevsetMode && 250 + !revsetResult.loading && 251 + !revsetResult.error && 252 + revsetResult.changeIds.length > 0 && ( 253 + <div className="px-3 py-2 text-xs text-muted-foreground border-b border-border"> 254 + revset: {revsetResult.label} ({revsetResult.changeIds.length} match 255 + {revsetResult.changeIds.length !== 1 ? "es" : ""}) 256 + </div> 257 + )} 250 258 <CommandGroup> 251 259 {filteredRevisions.map((revision) => { 252 260 const firstLine = revision.description?.split("\n")[0] || "(no description)"; 253 261 const matchType = getMatchType(revision); 254 262 const matchingBookmark = getMatchingBookmark(revision); 255 - 263 + 256 264 return ( 257 265 <CommandItem 258 266 key={revision.change_id}
+40 -34
apps/desktop/src/components/AppShell.tsx
··· 1 1 import { useAtom } from "@effect-atom/atom-react"; 2 2 import { useLiveQuery } from "@tanstack/react-db"; 3 - import { useQuery } from "@tanstack/react-query"; 4 3 import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; 5 4 import { homeDir } from "@tauri-apps/api/path"; 6 5 import { getCurrentWindow } from "@tauri-apps/api/window"; ··· 38 37 39 38 import { 40 39 abandonRevision, 40 + addRepository, 41 41 editRevision, 42 42 emptyChangesCollection, 43 + emptyCommitRecencyCollection, 43 44 emptyRevisionsCollection, 45 + getCommitRecencyCollection, 44 46 getRevisionChangesCollection, 45 47 getRevisionsCollection, 46 48 newRevision, ··· 50 52 import { 51 53 findRepository, 52 54 findRepositoryByPath, 53 - getCommitRecency, 54 55 type Repository, 55 56 type Revision, 56 - upsertRepository, 57 57 } from "@/tauri-commands"; 58 58 59 59 const openDirectoryDialogEffect = Effect.gen(function* () { ··· 126 126 const { data: revisions = [], isLoading = false } = useLiveQuery(revisionsCollection); 127 127 128 128 // Fetch commit recency data for branch ordering 129 - const { data: commitRecency } = useQuery({ 130 - queryKey: ["commit-recency", activeProject?.path], 131 - queryFn: () => { 132 - if (!activeProject?.path) throw new Error("No repo path"); 133 - return getCommitRecency(activeProject.path, 500); // Walk last 500 ops 134 - }, 135 - enabled: !!activeProject?.path, 136 - staleTime: 30000, // Cache for 30s 137 - }); 129 + const commitRecencyCollection = activeProject?.path 130 + ? getCommitRecencyCollection(activeProject.path) 131 + : emptyCommitRecencyCollection; 132 + const { data: commitRecencyEntries = [] } = useLiveQuery(commitRecencyCollection); 133 + const commitRecency = commitRecencyEntries[0]?.data ?? undefined; 138 134 139 135 const orderedRevisions = reorderForGraph(revisions, commitRecency); 140 136 ··· 156 152 } 157 153 158 154 if (hiddenChangeIds.size === 0) return orderedRevisions; 159 - return orderedRevisions.filter(r => !hiddenChangeIds.has(r.change_id)); 155 + return orderedRevisions.filter((r) => !hiddenChangeIds.has(r.change_id)); 160 156 }, [revisions, orderedRevisions, expandedStacks]); 161 157 162 158 // Debug: log when revisions change to track reordering ··· 170 166 const changes: string[] = []; 171 167 for (let i = 0; i < Math.min(currentOrder.length, prevOrder.length); i++) { 172 168 if (currentOrder[i] !== prevOrder[i]) { 173 - changes.push(`[${i}] ${prevOrder[i]?.slice(0, 4) ?? "?"} → ${currentOrder[i]?.slice(0, 4) ?? "?"}`); 169 + changes.push( 170 + `[${i}] ${prevOrder[i]?.slice(0, 4) ?? "?"} → ${currentOrder[i]?.slice(0, 4) ?? "?"}`, 171 + ); 174 172 if (changes.length >= 10) break; 175 173 } 176 174 } ··· 179 177 console.log("[reorder] changes detected:", { 180 178 prevLength: prevOrder.length, 181 179 newLength: currentOrder.length, 182 - wcBefore: prevOrder.findIndex((id) => revisions.find((r) => r.change_id === id)?.is_working_copy), 180 + wcBefore: prevOrder.findIndex( 181 + (id) => revisions.find((r) => r.change_id === id)?.is_working_copy, 182 + ), 183 183 wcAfter: currentOrder.findIndex((id) => id === workingCopy?.change_id), 184 184 firstChanges: changes, 185 185 first10: currentOrder.slice(0, 10).map((id) => id.slice(0, 4)), ··· 229 229 }; 230 230 231 231 yield* Effect.tryPromise({ 232 - try: () => upsertRepository(repository), 232 + try: () => addRepository(repositoriesCollection, repository), 233 233 catch: (error) => new Error(`Failed to save repository: ${error}`), 234 234 }); 235 235 236 236 yield* Effect.sync(() => { 237 - repositoriesCollection.utils.writeUpsert([repository]); 238 237 navigate({ to: "/project/$projectId", params: { projectId: repositoryId } }); 239 238 }); 240 239 }).pipe( ··· 272 271 selectedChangeId: rev ?? null, 273 272 onNavigate: handleNavigateToChangeId, 274 273 scrollToChangeId: (changeId) => revisionGraphRef.current?.scrollToChangeId(changeId), 274 + disableBasicNavigation: true, // j/k/arrows handled in RevisionGraph for display row awareness 275 275 }); 276 276 277 277 function triggerFlash(changeId: string) { ··· 310 310 311 311 // Debug: log indices before edit 312 312 const currentWCIndex = orderedRevisions.findIndex((r) => r.change_id === currentWC?.change_id); 313 - const targetIndex = orderedRevisions.findIndex((r) => r.change_id === selectedRevision.change_id); 313 + const targetIndex = orderedRevisions.findIndex( 314 + (r) => r.change_id === selectedRevision.change_id, 315 + ); 314 316 console.log("[edit] before:", { 315 317 currentWC: currentWC?.change_id_short, 316 318 currentWCIndex, ··· 346 348 if (!activeProject || !pendingAbandon) return; 347 349 const preset = activeProject.revset_preset ?? "full_history"; 348 350 const limit = preset === "full_history" ? 10000 : 100; 349 - abandonRevision(revisionsCollection, activeProject.path, pendingAbandon, limit, stackRevset, preset); 351 + abandonRevision( 352 + revisionsCollection, 353 + activeProject.path, 354 + pendingAbandon, 355 + limit, 356 + stackRevset, 357 + preset, 358 + ); 350 359 setPendingAbandon(null); 351 360 } 352 361 ··· 565 574 ref={revisionGraphRef} 566 575 revisions={revisions} 567 576 selectedRevision={selectedRevision} 568 - onSelectRevision={handleSelectRevision} 569 - isLoading={isLoading} 570 - flash={flash} 571 - repoPath={activeProject?.path ?? null} 572 - pendingAbandon={pendingAbandon} 573 - /> 577 + onSelectRevision={handleSelectRevision} 578 + isLoading={isLoading} 579 + flash={flash} 580 + repoPath={activeProject?.path ?? null} 581 + pendingAbandon={pendingAbandon} 582 + /> 574 583 </section> 575 584 </ResizablePanel> 576 585 <ResizableHandle withHandle /> 577 586 <ResizablePanel defaultSize={isNarrowScreen ? 60 : 67} minSize={30}> 578 - <aside 579 - className="h-full" 580 - aria-label="Diff viewer" 581 - > 582 - <PrerenderedDiffPanel 583 - repoPath={activeProject?.path ?? null} 584 - revisions={orderedRevisions} 585 - selectedChangeId={selectedRevision?.change_id ?? null} 586 - /> 587 + <aside className="h-full" aria-label="Diff viewer"> 588 + <PrerenderedDiffPanel 589 + repoPath={activeProject?.path ?? null} 590 + revisions={orderedRevisions} 591 + selectedChangeId={selectedRevision?.change_id ?? null} 592 + /> 587 593 </aside> 588 594 </ResizablePanel> 589 595 </ResizablePanelGroup>
+50 -7
apps/desktop/src/components/ChangedFilesList.tsx
··· 1 + import { CheckIcon } from "lucide-react"; 1 2 import { Skeleton } from "@/components/ui/skeleton"; 2 3 import { cn } from "@/lib/utils"; 3 4 import type { ChangedFile } from "@/schemas"; ··· 7 8 selectedFile: string | null; 8 9 onSelectFile: (path: string) => void; 9 10 isLoading?: boolean; 11 + /** Set of file paths that are "selected" (checked) */ 12 + selectedFiles?: Set<string>; 13 + /** Called when a file's selection state changes */ 14 + onToggleFileSelection?: (path: string) => void; 15 + /** Whether to show selection checkboxes */ 16 + showSelection?: boolean; 10 17 } 11 18 12 19 function StatusIndicator({ status }: { status: ChangedFile["status"] }) { ··· 41 48 42 49 function FileListItem({ 43 50 file, 44 - isSelected, 51 + isFocused, 52 + isChecked, 45 53 onClick, 54 + onToggleSelection, 55 + showSelection, 46 56 }: { 47 57 file: ChangedFile; 48 - isSelected: boolean; 58 + isFocused: boolean; 59 + isChecked: boolean; 49 60 onClick: () => void; 61 + onToggleSelection?: () => void; 62 + showSelection?: boolean; 50 63 }) { 51 64 return ( 52 65 <button 53 66 type="button" 54 - onClick={onClick} 55 67 className={cn( 56 68 "flex items-center gap-2 w-full px-3 py-1.5 text-left transition-colors cursor-pointer group", 57 69 "hover:bg-muted/50", 58 - isSelected && "bg-muted text-foreground", 70 + isFocused && "bg-muted text-foreground", 59 71 )} 72 + onClick={onClick} 60 73 > 74 + {showSelection && ( 75 + <button 76 + type="button" 77 + onClick={(e) => { 78 + e.stopPropagation(); 79 + onToggleSelection?.(); 80 + }} 81 + className={cn( 82 + "flex items-center justify-center w-4 h-4 border rounded-sm shrink-0 transition-colors", 83 + isChecked 84 + ? "bg-primary border-primary text-primary-foreground" 85 + : "border-muted-foreground/40 hover:border-muted-foreground", 86 + )} 87 + title={isChecked ? "Deselect file" : "Select file"} 88 + > 89 + {isChecked && <CheckIcon className="size-3" />} 90 + </button> 91 + )} 61 92 <StatusIndicator status={file.status} /> 62 93 <span 63 94 className={cn( 64 95 "font-mono text-xs truncate flex-1", 65 - isSelected ? "text-foreground" : "text-muted-foreground group-hover:text-foreground", 96 + isFocused ? "text-foreground" : "text-muted-foreground group-hover:text-foreground", 66 97 )} 67 98 title={file.path} 68 99 > ··· 96 127 selectedFile, 97 128 onSelectFile, 98 129 isLoading = false, 130 + selectedFiles, 131 + onToggleFileSelection, 132 + showSelection = false, 99 133 }: ChangedFilesListProps) { 100 134 if (isLoading) { 101 135 return ( ··· 121 155 122 156 const filesCount = files.length; 123 157 const fileWord = filesCount === 1 ? "file" : "files"; 158 + const selectedCount = selectedFiles?.size ?? 0; 124 159 125 160 return ( 126 161 <div className="flex flex-col"> 127 - <div className="px-3 py-2 border-b border-border"> 162 + <div className="px-3 py-2 border-b border-border flex items-center justify-between"> 128 163 <span className="text-xs font-semibold text-muted-foreground"> 129 164 {filesCount} {fileWord} changed 130 165 </span> 166 + {showSelection && selectedCount > 0 && ( 167 + <span className="text-xs text-primary font-medium">{selectedCount} selected</span> 168 + )} 131 169 </div> 132 170 <div className="flex flex-col"> 133 171 {files.map((file) => ( 134 172 <FileListItem 135 173 key={file.path} 136 174 file={file} 137 - isSelected={selectedFile === file.path} 175 + isFocused={selectedFile === file.path} 176 + isChecked={selectedFiles?.has(file.path) ?? false} 138 177 onClick={() => onSelectFile(file.path)} 178 + onToggleSelection={ 179 + onToggleFileSelection ? () => onToggleFileSelection(file.path) : undefined 180 + } 181 + showSelection={showSelection} 139 182 /> 140 183 ))} 141 184 </div>
+5 -1
apps/desktop/src/components/CommandPalette.tsx
··· 16 16 onOpenSettings: () => void; 17 17 } 18 18 19 - export function CommandPalette({ onOpenRepo, onOpenProjects, onOpenSettings }: CommandPaletteProps) { 19 + export function CommandPalette({ 20 + onOpenRepo, 21 + onOpenProjects, 22 + onOpenSettings, 23 + }: CommandPaletteProps) { 20 24 const [open, setOpen] = useState(false); 21 25 22 26 useKeyboardShortcut({
+198 -24
apps/desktop/src/components/DiffPanel.tsx
··· 1 1 import { PatchDiff } from "@pierre/diffs/react"; 2 - import { useQuery } from "@tanstack/react-query"; 2 + import { useLiveQuery } from "@tanstack/react-db"; 3 3 import { useSearch } from "@tanstack/react-router"; 4 + import { 5 + ChevronDownIcon, 6 + ChevronRightIcon, 7 + ChevronsDownUpIcon, 8 + ChevronsUpDownIcon, 9 + ColumnsIcon, 10 + RowsIcon, 11 + } from "lucide-react"; 4 12 import { useEffect, useRef, useState } from "react"; 13 + import { Button } from "@/components/ui/button"; 14 + import { emptyDiffCollection, getRevisionDiffCollection } from "@/db"; 5 15 import type { Revision } from "@/tauri-commands"; 6 - import { getRevisionDiff } from "@/tauri-commands"; 7 16 8 17 interface DiffPanelProps { 9 18 repoPath: string | null; ··· 15 24 const commitIdShort = revision.commit_id.substring(0, 12); 16 25 17 26 return ( 18 - <div className="border border-border rounded-lg p-4 mb-4 bg-muted/50"> 19 - <div className="font-mono text-sm space-y-2"> 27 + <div className="border border-border rounded-lg mb-4 bg-muted/50"> 28 + <div className="px-3 py-2 font-mono text-sm space-y-2"> 20 29 <div className="flex gap-4"> 21 30 <div> 22 31 <span className="text-muted-foreground">Change ID:</span>{" "} ··· 50 59 return match ? match[1] : "unknown"; 51 60 } 52 61 62 + type DiffStyle = "unified" | "split"; 63 + 53 64 function FileDiffSection({ 54 65 patch, 55 66 defaultCollapsed = false, 56 67 isSelected = false, 57 68 fileRef, 69 + globalDiffStyle, 70 + onCollapseChange, 58 71 }: { 59 72 patch: string; 60 73 defaultCollapsed?: boolean; 61 74 isSelected?: boolean; 62 75 fileRef?: React.RefObject<HTMLDivElement | null>; 76 + globalDiffStyle: DiffStyle; 77 + onCollapseChange?: (collapsed: boolean) => void; 63 78 }) { 64 79 const [isCollapsedByUser, setIsCollapsedByUser] = useState(defaultCollapsed); 80 + const [localDiffStyle, setLocalDiffStyle] = useState<DiffStyle | null>(null); 65 81 const filePath = extractFilePath(patch); 66 82 67 83 // Derived state: auto-expand when selected 68 84 const isCollapsed = isSelected ? false : isCollapsedByUser; 69 85 86 + // Use local override if set, otherwise use global 87 + const effectiveDiffStyle = localDiffStyle ?? globalDiffStyle; 88 + 89 + function handleToggleCollapse() { 90 + const newCollapsed = !isCollapsed; 91 + setIsCollapsedByUser(newCollapsed); 92 + onCollapseChange?.(newCollapsed); 93 + } 94 + 70 95 return ( 71 96 <div 72 97 ref={fileRef} ··· 74 99 isSelected ? "border-accent-foreground border-2" : "border-border" 75 100 }`} 76 101 > 77 - <button 78 - type="button" 79 - onClick={() => setIsCollapsedByUser(!isCollapsed)} 80 - className={`w-full px-4 py-2 border-b hover:bg-accent/50 transition-colors flex items-center justify-between ${ 102 + <div 103 + className={`flex items-center gap-2 px-2 py-1.5 border-b ${ 81 104 isSelected ? "bg-accent border-accent-foreground" : "bg-muted border-border" 82 105 }`} 83 106 > 84 - <code className="font-mono text-sm text-foreground text-left">{filePath}</code> 85 - <span className="text-xs text-muted-foreground">{isCollapsed ? "▶" : "▼"}</span> 86 - </button> 107 + {/* Collapse toggle button - covers left side */} 108 + <button 109 + type="button" 110 + onClick={handleToggleCollapse} 111 + className="flex items-center gap-2 flex-1 min-w-0 hover:bg-accent/50 -m-1.5 -ml-2 p-1.5 pl-2 rounded-l transition-colors" 112 + > 113 + <span className="text-muted-foreground shrink-0"> 114 + {isCollapsed ? ( 115 + <ChevronRightIcon className="size-4" /> 116 + ) : ( 117 + <ChevronDownIcon className="size-4" /> 118 + )} 119 + </span> 120 + <code className="font-mono text-sm text-foreground text-left flex-1 truncate"> 121 + {filePath} 122 + </code> 123 + </button> 124 + 125 + {/* Per-file diff style toggle buttons */} 126 + <div className="flex items-center gap-0.5"> 127 + <Button 128 + variant={effectiveDiffStyle === "unified" ? "secondary" : "ghost"} 129 + size="icon-xs" 130 + onClick={() => setLocalDiffStyle("unified")} 131 + title="Unified diff" 132 + > 133 + <RowsIcon className="size-3" /> 134 + </Button> 135 + <Button 136 + variant={effectiveDiffStyle === "split" ? "secondary" : "ghost"} 137 + size="icon-xs" 138 + onClick={() => setLocalDiffStyle("split")} 139 + title="Split diff" 140 + > 141 + <ColumnsIcon className="size-3" /> 142 + </Button> 143 + </div> 144 + </div> 87 145 {!isCollapsed && ( 88 146 <div> 89 147 {!patch.trim() ? ( ··· 91 149 No changes in this file 92 150 </div> 93 151 ) : ( 94 - <PatchDiff patch={patch} options={{ hunkSeparators: "line-info" }} /> 152 + <PatchDiff 153 + patch={patch} 154 + options={{ hunkSeparators: "line-info", diffStyle: effectiveDiffStyle }} 155 + /> 95 156 )} 96 157 </div> 97 158 )} ··· 124 185 return fileDiffs; 125 186 } 126 187 188 + interface PrerenderedDiffPanelProps { 189 + repoPath: string | null; 190 + revisions: Revision[]; 191 + selectedChangeId: string | null; 192 + } 193 + 194 + export function PrerenderedDiffPanel({ 195 + repoPath, 196 + revisions, 197 + selectedChangeId, 198 + }: PrerenderedDiffPanelProps) { 199 + const selectedRevision = selectedChangeId 200 + ? (revisions.find((r) => r.change_id === selectedChangeId) ?? null) 201 + : null; 202 + 203 + return <DiffPanel repoPath={repoPath} changeId={selectedChangeId} revision={selectedRevision} />; 204 + } 205 + 206 + function DiffToolbar({ 207 + allCollapsed, 208 + onToggleAllFolds, 209 + diffStyle, 210 + onDiffStyleChange, 211 + }: { 212 + allCollapsed: boolean; 213 + onToggleAllFolds: () => void; 214 + diffStyle: DiffStyle; 215 + onDiffStyleChange: (style: DiffStyle) => void; 216 + }) { 217 + return ( 218 + <div className="flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30 sticky top-0 z-10"> 219 + <div className="flex items-center gap-2"> 220 + <Button 221 + variant="ghost" 222 + size="xs" 223 + onClick={onToggleAllFolds} 224 + title={allCollapsed ? "Expand all files" : "Collapse all files"} 225 + > 226 + {allCollapsed ? ( 227 + <ChevronsUpDownIcon className="size-3.5" /> 228 + ) : ( 229 + <ChevronsDownUpIcon className="size-3.5" /> 230 + )} 231 + <span>{allCollapsed ? "Expand all" : "Collapse all"}</span> 232 + </Button> 233 + </div> 234 + <div className="flex items-center gap-1"> 235 + <span className="text-xs text-muted-foreground mr-1">View:</span> 236 + <Button 237 + variant={diffStyle === "unified" ? "secondary" : "ghost"} 238 + size="icon-xs" 239 + onClick={() => onDiffStyleChange("unified")} 240 + title="Unified diff view" 241 + > 242 + <RowsIcon className="size-3" /> 243 + </Button> 244 + <Button 245 + variant={diffStyle === "split" ? "secondary" : "ghost"} 246 + size="icon-xs" 247 + onClick={() => onDiffStyleChange("split")} 248 + title="Split diff view" 249 + > 250 + <ColumnsIcon className="size-3" /> 251 + </Button> 252 + </div> 253 + </div> 254 + ); 255 + } 256 + 127 257 export function DiffPanel({ repoPath, changeId, revision }: DiffPanelProps) { 128 258 const { file: selectedFilePath } = useSearch({ strict: false }); 129 259 const fileRefsMap = useRef<Map<string, React.RefObject<HTMLDivElement | null>>>(new Map()); 260 + const [globalDiffStyle, setGlobalDiffStyle] = useState<DiffStyle>("unified"); 261 + const [collapsedFiles, setCollapsedFiles] = useState<Set<string>>(new Set()); 262 + const [lastChangeId, setLastChangeId] = useState<string | null>(null); 263 + 264 + // Reset collapsed state when revision changes 265 + if (changeId !== lastChangeId) { 266 + setLastChangeId(changeId); 267 + if (collapsedFiles.size > 0) { 268 + setCollapsedFiles(new Set()); 269 + } 270 + } 130 271 131 272 // Always fetch all diffs 132 - const { data: revisionDiff = "", isLoading } = useQuery({ 133 - queryKey: ["revision-diff", repoPath, changeId], 134 - queryFn: () => { 135 - if (!repoPath || !changeId) { 136 - throw new Error("Missing required parameters"); 137 - } 138 - return getRevisionDiff(repoPath, changeId); 139 - }, 140 - enabled: Boolean(repoPath && changeId), 141 - }); 273 + const diffCollection = 274 + repoPath && changeId ? getRevisionDiffCollection(repoPath, changeId) : emptyDiffCollection; 275 + const { data: diffEntries = [], isLoading } = useLiveQuery(diffCollection); 276 + const revisionDiff = diffEntries[0]?.content ?? ""; 142 277 143 278 const fileDiffs = splitMultiFileDiff(revisionDiff); 279 + const filePaths = fileDiffs.map(extractFilePath); 144 280 145 281 // Get or create ref for each file 146 282 const getFileRef = (filePath: string): React.RefObject<HTMLDivElement | null> => { ··· 151 287 return fileRefsMap.current.get(filePath)!; 152 288 }; 153 289 290 + // Track collapse state for toggle all 291 + const allCollapsed = filePaths.length > 0 && filePaths.every((p) => collapsedFiles.has(p)); 292 + 293 + function handleToggleAllFolds() { 294 + if (allCollapsed) { 295 + // Expand all 296 + setCollapsedFiles(new Set()); 297 + } else { 298 + // Collapse all 299 + setCollapsedFiles(new Set(filePaths)); 300 + } 301 + } 302 + 303 + function handleFileCollapseChange(filePath: string, collapsed: boolean) { 304 + setCollapsedFiles((prev) => { 305 + const next = new Set(prev); 306 + if (collapsed) { 307 + next.add(filePath); 308 + } else { 309 + next.delete(filePath); 310 + } 311 + return next; 312 + }); 313 + } 314 + 154 315 // Scroll to selected file when it changes 155 316 useEffect(() => { 156 317 if (selectedFilePath && fileRefsMap.current.has(selectedFilePath)) { ··· 190 351 191 352 return ( 192 353 <div className="h-full overflow-auto bg-background"> 354 + {revision && ( 355 + <div className="pt-6 px-4 pb-0"> 356 + <RevisionHeader revision={revision} /> 357 + </div> 358 + )} 359 + <DiffToolbar 360 + allCollapsed={allCollapsed} 361 + onToggleAllFolds={handleToggleAllFolds} 362 + diffStyle={globalDiffStyle} 363 + onDiffStyleChange={setGlobalDiffStyle} 364 + /> 193 365 <div className="p-4 space-y-4"> 194 - {revision && <RevisionHeader revision={revision} />} 195 366 {fileDiffs.map((patch, idx) => { 196 367 const filePath = extractFilePath(patch); 197 368 const fileRef = getFileRef(filePath); 198 369 const isSelected = selectedFilePath === filePath; 370 + const isCollapsed = collapsedFiles.has(filePath); 199 371 200 372 return ( 201 373 <FileDiffSection 202 374 key={filePath} 203 375 patch={patch} 204 - defaultCollapsed={idx > 0 && !isSelected} 376 + defaultCollapsed={isCollapsed || (idx > 0 && !isSelected)} 205 377 isSelected={isSelected} 206 378 fileRef={fileRef} 379 + globalDiffStyle={globalDiffStyle} 380 + onCollapseChange={(collapsed) => handleFileCollapseChange(filePath, collapsed)} 207 381 /> 208 382 ); 209 383 })}
+587 -175
apps/desktop/src/components/RevisionGraph.tsx
··· 1 1 import { useAtom } from "@effect-atom/atom-react"; 2 - import { useQuery } from "@tanstack/react-query"; 2 + import { useLiveQuery } from "@tanstack/react-db"; 3 3 import { useNavigate, useSearch } from "@tanstack/react-router"; 4 4 import { useVirtualizer } from "@tanstack/react-virtual"; 5 5 import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; 6 6 import { expandedStacksAtom, inlineJumpQueryAtom } from "@/atoms"; 7 7 import { ChangedFilesList } from "@/components/ChangedFilesList"; 8 - import { reorderForGraph, detectStacks, computeRevisionAncestry, type RevisionStack } from "@/components/revision-graph-utils"; 9 - import { prefetchRevisionDiffs } from "@/db"; 8 + import { 9 + reorderForGraph, 10 + detectStacks, 11 + computeRevisionAncestry, 12 + type RevisionStack, 13 + } from "@/components/revision-graph-utils"; 14 + import { emptyChangesCollection, getRevisionChangesCollection, prefetchRevisionDiffs } from "@/db"; 10 15 import { useKeyboardShortcut } from "@/hooks/useKeyboard"; 11 - import { getRevisionChanges, type Revision } from "@/tauri-commands"; 12 - 16 + import type { Revision } from "@/tauri-commands"; 13 17 14 18 // Debug overlay - toggle with Ctrl+Shift+D 15 19 const DEBUG_OVERLAY_DEFAULT = false; ··· 165 169 } 166 170 167 171 const ROW_HEIGHT = 64; 168 - const COLLAPSED_INDICATOR_HEIGHT = 32; 169 172 const LANE_WIDTH = 20; 170 173 const LANE_PADDING = 8; 171 174 const NODE_RADIUS = 5; ··· 216 219 function GraphNode({ revision, lane, isSelected, color }: GraphNodeProps) { 217 220 const isWorkingCopy = revision.is_working_copy; 218 221 const isImmutable = revision.is_immutable; 219 - 222 + 220 223 const size = isWorkingCopy ? NODE_RADIUS * 2 + 6 : NODE_RADIUS * 2; 221 224 const selectedRingSize = isWorkingCopy ? NODE_RADIUS + 6 : NODE_RADIUS + 4; 222 225 ··· 319 322 fillOpacity={0.3} 320 323 /> 321 324 )} 322 - <circle 323 - cx={(size + 8) / 2} 324 - cy={(size + 8) / 2} 325 - r={NODE_RADIUS} 326 - fill={color} 327 - /> 325 + <circle cx={(size + 8) / 2} cy={(size + 8) / 2} r={NODE_RADIUS} fill={color} /> 328 326 </svg> 329 327 ); 330 328 } ··· 341 339 } 342 340 343 341 // GraphEdge - Semantic edge component with source/target revision bindings 344 - function GraphEdge({ binding, sourceY, targetY, sourceRevision, targetRevision, stackTopY, stackBottomY, onToggleStack }: GraphEdgeProps) { 345 - const { sourceLane, targetLane, edgeType, isDeemphasized, isMissingStub, collapsedStackId, collapsedCount, expandedStackId } = binding; 346 - 342 + function GraphEdge({ 343 + binding, 344 + sourceY, 345 + targetY, 346 + sourceRevision, 347 + targetRevision, 348 + stackTopY, 349 + stackBottomY, 350 + onToggleStack, 351 + }: GraphEdgeProps) { 352 + const { 353 + sourceLane, 354 + targetLane, 355 + edgeType, 356 + isDeemphasized, 357 + isMissingStub, 358 + collapsedStackId, 359 + collapsedCount, 360 + expandedStackId, 361 + } = binding; 362 + 347 363 const sourceX = laneToX(sourceLane); 348 364 const targetX = laneToX(targetLane); 349 365 const sourceColor = laneColor(sourceLane); ··· 390 406 const isCollapsedStack = !!collapsedStackId; 391 407 const y1 = sourceY + NODE_RADIUS; 392 408 const y2 = targetY - NODE_RADIUS; 393 - 409 + 394 410 // For collapsed stacks, draw a dotted line with clickable area 395 411 if (isCollapsedStack) { 396 412 const collapsedLabel = `${collapsedCount ?? 0} hidden revision${(collapsedCount ?? 0) !== 1 ? "s" : ""} - click to expand`; 397 - 413 + 398 414 return ( 399 - <g 415 + <g 400 416 aria-label={collapsedLabel} 401 417 className="cursor-pointer group" 402 418 style={{ pointerEvents: "auto" }} ··· 404 420 > 405 421 <title>{collapsedLabel}</title> 406 422 {/* Invisible wider hitbox for easier clicking */} 407 - <line 408 - x1={sourceX} 409 - y1={y1} 410 - x2={sourceX} 411 - y2={y2} 412 - stroke="transparent" 413 - strokeWidth={16} 414 - /> 423 + <line x1={sourceX} y1={y1} x2={sourceX} y2={y2} stroke="transparent" strokeWidth={16} /> 415 424 {/* Visible dotted line */} 416 425 <line 417 426 x1={sourceX} ··· 432 441 </g> 433 442 ); 434 443 } 435 - 444 + 436 445 // For expanded stacks, make the edge clickable to collapse 437 446 if (expandedStackId) { 438 447 const expandedLabel = `Click to collapse stack`; ··· 440 449 const hitboxY1 = stackTopY !== undefined ? stackTopY : y1; 441 450 const hitboxY2 = stackBottomY !== undefined ? stackBottomY : y2; 442 451 return ( 443 - <g 452 + <g 444 453 aria-label={expandedLabel} 445 454 className="cursor-pointer stack-group" 446 455 data-stack-id={expandedStackId} ··· 475 484 </g> 476 485 ); 477 486 } 478 - 487 + 479 488 return ( 480 489 <g aria-label={ariaLabel} style={{ pointerEvents: "none" }}> 481 490 <title>{ariaLabel}</title> ··· 550 559 onToggleStack, 551 560 }: EdgeLayerProps) { 552 561 const svgRef = useRef<SVGSVGElement>(null); 553 - 562 + 554 563 // Add overscan for edges that might span across viewport boundary 555 564 // Use larger overscan to handle collapsed stack edges that span many rows 556 565 const overscan = 15; ··· 562 571 const sourceRow = commitToRow.get(binding.sourceRevisionId); 563 572 const targetRow = commitToRow.get(binding.targetRevisionId); 564 573 if (sourceRow === undefined) return false; 565 - 574 + 566 575 // For missing stubs, just check if source is near visible range 567 576 if (binding.isMissingStub) { 568 577 return sourceRow >= startRow && sourceRow <= endRow; 569 578 } 570 - 579 + 571 580 if (targetRow === undefined) return false; 572 - 581 + 573 582 // Check if edge passes through visible area 574 583 const minRow = Math.min(sourceRow, targetRow); 575 584 const maxRow = Math.max(sourceRow, targetRow); ··· 587 596 const handleMouseOver = (e: Event) => { 588 597 const target = e.target as HTMLElement; 589 598 // Check if the event originated from a stack group 590 - const group = target.closest('g.stack-group[data-stack-id]') as HTMLElement; 599 + const group = target.closest("g.stack-group[data-stack-id]") as HTMLElement; 591 600 if (!group) return; 592 - 593 - const stackId = group.getAttribute('data-stack-id'); 601 + 602 + const stackId = group.getAttribute("data-stack-id"); 594 603 if (!stackId || stackId === hoveredStackId) return; 595 604 596 605 hoveredStackId = stackId; 597 606 // Find all edges with the same stack-id and add hover class 598 607 const edges = svg.querySelectorAll(`line.stack-edge[data-stack-id="${stackId}"]`); 599 608 edges.forEach((edge) => { 600 - edge.classList.add('stack-edge-hovered'); 609 + edge.classList.add("stack-edge-hovered"); 601 610 }); 602 611 }; 603 612 604 613 const handleMouseOut = (e: Event) => { 605 614 const target = e.target as HTMLElement; 606 615 const relatedTarget = (e as MouseEvent).relatedTarget as HTMLElement; 607 - 616 + 608 617 // Check if we're leaving a stack group 609 - const group = target.closest('g.stack-group[data-stack-id]') as HTMLElement; 618 + const group = target.closest("g.stack-group[data-stack-id]") as HTMLElement; 610 619 if (!group) return; 611 - 620 + 612 621 // Check if we're moving to another element within the same stack group 613 622 if (relatedTarget && group.contains(relatedTarget)) return; 614 - 615 - const stackId = group.getAttribute('data-stack-id'); 623 + 624 + const stackId = group.getAttribute("data-stack-id"); 616 625 if (!stackId || stackId !== hoveredStackId) return; 617 626 618 627 hoveredStackId = null; 619 628 // Remove hover class from all edges with the same stack-id 620 629 const edges = svg.querySelectorAll(`line.stack-edge[data-stack-id="${stackId}"]`); 621 630 edges.forEach((edge) => { 622 - edge.classList.remove('stack-edge-hovered'); 631 + edge.classList.remove("stack-edge-hovered"); 623 632 }); 624 633 }; 625 634 626 635 // Use event delegation - attach listeners to the SVG element 627 636 // mouseover/mouseout bubble, unlike mouseenter/mouseleave 628 - svg.addEventListener('mouseover', handleMouseOver, true); 629 - svg.addEventListener('mouseout', handleMouseOut, true); 637 + svg.addEventListener("mouseover", handleMouseOver, true); 638 + svg.addEventListener("mouseout", handleMouseOut, true); 630 639 631 640 return () => { 632 - svg.removeEventListener('mouseover', handleMouseOver, true); 633 - svg.removeEventListener('mouseout', handleMouseOut, true); 641 + svg.removeEventListener("mouseover", handleMouseOver, true); 642 + svg.removeEventListener("mouseout", handleMouseOut, true); 634 643 }; 635 644 }, []); 636 645 ··· 646 655 <title>Revision graph edges</title> 647 656 {visibleBindings.map((binding) => { 648 657 const sourceRow = commitToRow.get(binding.sourceRevisionId); 649 - const targetRow = binding.isMissingStub 650 - ? (sourceRow !== undefined ? sourceRow + 1 : undefined) 658 + const targetRow = binding.isMissingStub 659 + ? sourceRow !== undefined 660 + ? sourceRow + 1 661 + : undefined 651 662 : commitToRow.get(binding.targetRevisionId); 652 - 663 + 653 664 if (sourceRow === undefined) return null; 654 - 665 + 655 666 const sourceRevision = revisionMap.get(binding.sourceRevisionId); 656 - const targetRevision = binding.targetRevisionId 657 - ? revisionMap.get(binding.targetRevisionId) ?? null 667 + const targetRevision = binding.targetRevisionId 668 + ? (revisionMap.get(binding.targetRevisionId) ?? null) 658 669 : null; 659 - 670 + 660 671 if (!sourceRevision) return null; 661 672 662 673 // For expanded stacks, calculate full stack bounds ··· 681 692 key={binding.id} 682 693 binding={binding} 683 694 sourceY={getRowCenter(sourceRow)} 684 - targetY={targetRow !== undefined ? getRowCenter(targetRow) : getRowCenter(sourceRow) + ROW_HEIGHT} 695 + targetY={ 696 + targetRow !== undefined 697 + ? getRowCenter(targetRow) 698 + : getRowCenter(sourceRow) + ROW_HEIGHT 699 + } 685 700 sourceRevision={sourceRevision} 686 701 targetRevision={targetRevision} 687 702 stackTopY={stackTopY} ··· 948 963 // Generate semantic edge bindings from nodes' parent connections 949 964 const edgeBindings: EdgeBinding[] = []; 950 965 let edgeCounter = 0; 951 - 966 + 952 967 for (const node of nodes) { 953 968 for (const conn of node.parentConnections) { 954 969 // For missing stubs, use commit_id of source and empty target 955 - const targetCommitId = conn.isMissingStub 956 - ? "" 957 - : rows[conn.parentRow]?.revision.commit_id ?? ""; 958 - 970 + const targetCommitId = conn.isMissingStub 971 + ? "" 972 + : (rows[conn.parentRow]?.revision.commit_id ?? ""); 973 + 959 974 edgeBindings.push({ 960 975 id: `edge-${node.revision.commit_id}-${edgeCounter++}`, 961 976 sourceRevisionId: node.revision.commit_id, ··· 985 1000 lane, 986 1001 maxLaneOnRow, 987 1002 isSelected, 1003 + isChecked, 988 1004 onSelect, 989 1005 isFlashing, 990 1006 isDimmed, ··· 1000 1016 lane: number; 1001 1017 maxLaneOnRow: number; 1002 1018 isSelected: boolean; 1003 - onSelect: (changeId: string) => void; 1019 + isChecked: boolean; 1020 + onSelect: (changeId: string, modifiers: { shift: boolean; meta: boolean }) => void; 1004 1021 isFlashing: boolean; 1005 1022 isDimmed: boolean; 1006 1023 isExpanded: boolean; ··· 1013 1030 }) { 1014 1031 const firstLine = revision.description.split("\n")[0] || "(no description)"; 1015 1032 const fullDescription = revision.description || "(no description)"; 1016 - 1033 + 1017 1034 // Calculate the node position area - leaves space for graph edges on the left 1018 1035 const nodeAreaWidth = LANE_PADDING + (maxLaneOnRow + 1) * LANE_WIDTH; 1019 1036 const nodeOffset = laneToX(lane); ··· 1023 1040 const search = useSearch({ strict: false }); 1024 1041 const navigate = useNavigate(); 1025 1042 1026 - const changedFilesQuery = useQuery({ 1027 - queryKey: ["revision-changes", repoPath, revision.change_id], 1028 - queryFn: () => { 1029 - if (!repoPath) throw new Error("No repository path"); 1030 - return getRevisionChanges(repoPath, revision.change_id); 1031 - }, 1032 - enabled: isExpanded && !!repoPath, 1033 - }); 1043 + const changedFilesCollection = 1044 + isExpanded && repoPath 1045 + ? getRevisionChangesCollection(repoPath, revision.change_id) 1046 + : emptyChangesCollection; 1047 + const changedFilesQuery = useLiveQuery(changedFilesCollection); 1034 1048 1035 1049 function handleSelectFile(filePath: string) { 1036 1050 navigate({ ··· 1042 1056 const TOP_PADDING = 16; 1043 1057 const CONTENT_MIN_HEIGHT = 56; 1044 1058 const nodeSize = revision.is_working_copy ? NODE_RADIUS * 2 + 14 : NODE_RADIUS * 2 + 8; 1045 - 1059 + 1046 1060 return ( 1047 1061 <div style={{ height: isExpanded ? "auto" : ROW_HEIGHT }} className="flex flex-col relative"> 1048 1062 {/* Graph node - absolutely positioned to align with edge layer */} 1049 1063 <div 1050 1064 className="absolute z-20 flex items-center justify-center" 1051 - style={{ 1065 + style={{ 1052 1066 left: nodeOffset - nodeSize / 2, 1053 1067 top: TOP_PADDING + CONTENT_MIN_HEIGHT / 2 - nodeSize / 2, 1054 1068 }} 1055 1069 > 1056 - <GraphNode 1057 - revision={revision} 1058 - lane={lane} 1059 - isSelected={isSelected} 1060 - color={color} 1061 - /> 1070 + <GraphNode revision={revision} lane={lane} isSelected={isSelected} color={color} /> 1062 1071 </div> 1063 1072 <div className="flex items-start min-h-[56px] pt-4"> 1064 1073 {/* Spacer for graph area */} 1065 1074 <div className="shrink-0" style={{ width: nodeAreaWidth }} /> 1066 1075 <div 1067 - className={`relative flex-1 mr-2 min-w-0 overflow-hidden rounded my-2 mx-1 ${ 1068 - isFocused ? "" : "border border-border" 1069 - } bg-card text-card-foreground shadow-sm transition-colors duration-150 hover:shadow hover:bg-accent/20 hover:cursor-pointer ${ 1076 + className={`relative flex-1 mr-2 min-w-0 overflow-hidden rounded my-2 mx-1 select-none border ${ 1077 + isFocused || isChecked 1078 + ? "bg-accent/40 border-accent/60 hover:bg-accent/50" 1079 + : "bg-card hover:bg-muted border-border" 1080 + } text-card-foreground shadow-sm hover:shadow hover:cursor-pointer ${ 1070 1081 revision.is_immutable ? "opacity-60" : "" 1071 - } ${isDimmed ? "opacity-40" : ""} ${isSelected ? "bg-accent/30" : ""} ${ 1072 - isFocused ? "ring-2 ring-ring/80 ring-offset-2 ring-offset-background" : "" 1073 - }`} 1074 - onClick={() => onSelect(revision.change_id)} 1082 + } ${isDimmed ? "opacity-40" : ""}`} 1083 + onClick={(e) => { 1084 + // Prevent text selection on shift+click 1085 + if (e.shiftKey) { 1086 + e.preventDefault(); 1087 + window.getSelection()?.removeAllRanges(); 1088 + } 1089 + onSelect(revision.change_id, { shift: e.shiftKey, meta: e.metaKey || e.ctrlKey }); 1090 + }} 1075 1091 > 1076 1092 <div className={`px-3 py-2 min-w-0 ${isPendingAbandon ? "blur-sm" : ""}`}> 1077 1093 <div className="flex items-center gap-2 flex-nowrap min-w-0"> ··· 1093 1109 {revision.change_id_short[jumpQuery.length]} 1094 1110 </span> 1095 1111 {/* Rest of the ID */} 1096 - <span> 1097 - {revision.change_id_short.slice(jumpQuery.length + 1)} 1098 - </span> 1112 + <span>{revision.change_id_short.slice(jumpQuery.length + 1)}</span> 1099 1113 </> 1100 1114 ) : ( 1101 1115 revision.change_id_short 1102 1116 )} 1103 1117 </code> 1104 1118 {revision.bookmarks.length > 0 && ( 1105 - <span 1119 + <span 1106 1120 className="text-xs text-primary font-medium truncate min-w-0 whitespace-nowrap" 1107 1121 title={revision.bookmarks.join(", ")} 1108 1122 > ··· 1133 1147 {isPendingAbandon && ( 1134 1148 <div className="absolute inset-0 flex items-center justify-center bg-destructive/10 rounded"> 1135 1149 <div className="text-sm font-medium text-destructive-foreground bg-destructive/90 px-3 py-1.5 rounded"> 1136 - Abandon this revision? <kbd className="ml-1 px-1 bg-background/20 rounded">Y</kbd> / <kbd className="px-1 bg-background/20 rounded">N</kbd> 1150 + Abandon this revision? <kbd className="ml-1 px-1 bg-background/20 rounded">Y</kbd> /{" "} 1151 + <kbd className="px-1 bg-background/20 rounded">N</kbd> 1137 1152 </div> 1138 1153 </div> 1139 1154 )} ··· 1210 1225 ref, 1211 1226 ) { 1212 1227 const parentRef = useRef<HTMLDivElement>(null); 1213 - const { nodes, laneCount, rows: allRows, edgeBindings } = useMemo( 1214 - () => buildGraph(revisions), 1215 - [revisions], 1216 - ); 1228 + const { 1229 + nodes, 1230 + laneCount, 1231 + rows: allRows, 1232 + edgeBindings, 1233 + } = useMemo(() => buildGraph(revisions), [revisions]); 1217 1234 const expanded = useSearch({ strict: false, select: (s) => s.expanded }); 1218 1235 const search = useSearch({ strict: false }); 1219 1236 const navigate = useNavigate(); ··· 1235 1252 // Track which stacks are expanded (empty = all collapsed by default) 1236 1253 const [expandedStacks, setExpandedStacks] = useAtom(expandedStacksAtom); 1237 1254 1255 + // Read focused stack and selection from URL params 1256 + const focusedStackId = useSearch({ strict: false, select: (s) => s.stack ?? null }); 1257 + const selectedParam = useSearch({ strict: false, select: (s) => s.selected ?? "" }); 1258 + const selectionAnchor = useSearch({ strict: false, select: (s) => s.selectionAnchor ?? null }); 1259 + const selectedRevisions = useMemo(() => { 1260 + if (!selectedParam) return new Set<string>(); 1261 + return new Set(selectedParam.split(",").filter(Boolean)); 1262 + }, [selectedParam]); 1263 + const hasSelection = selectedRevisions.size > 0; 1264 + 1265 + // Update URL with new selection 1266 + function setSelectedRevisions(updater: Set<string> | ((prev: Set<string>) => Set<string>)) { 1267 + const newSelection = typeof updater === "function" ? updater(selectedRevisions) : updater; 1268 + const selected = newSelection.size > 0 ? [...newSelection].join(",") : undefined; 1269 + navigate({ 1270 + search: { ...search, selected } as any, 1271 + replace: true, 1272 + }); 1273 + } 1274 + 1275 + // Update URL with focused stack 1276 + function setFocusedStackId(stackId: string | null) { 1277 + navigate({ 1278 + search: { 1279 + ...search, 1280 + stack: stackId ?? undefined, 1281 + rev: stackId ? undefined : search.rev, 1282 + } as any, 1283 + replace: true, 1284 + }); 1285 + } 1286 + 1287 + // Toggle a revision's checked state 1288 + function toggleRevisionCheck(changeId: string) { 1289 + const next = new Set(selectedRevisions); 1290 + if (next.has(changeId)) { 1291 + next.delete(changeId); 1292 + } else { 1293 + next.add(changeId); 1294 + } 1295 + setSelectedRevisions(next); 1296 + } 1297 + 1298 + // Clear selection when Escape is pressed (when not in jump mode) 1299 + useKeyboardShortcut({ 1300 + key: "Escape", 1301 + modifiers: {}, 1302 + onPress: () => { 1303 + navigate({ 1304 + search: { 1305 + ...search, 1306 + selected: undefined, 1307 + selectionAnchor: undefined, 1308 + stack: undefined, 1309 + } as any, 1310 + replace: true, 1311 + }); 1312 + }, 1313 + enabled: (hasSelection || !!focusedStackId) && !inlineJumpMode, 1314 + }); 1315 + 1238 1316 // Build lookup maps for stacks 1239 1317 const { stackByChangeId, stackById, intermediateChangeIds } = useMemo(() => { 1240 1318 const byChangeId = new Map<string, RevisionStack>(); ··· 1262 1340 return map; 1263 1341 }, [nodes]); 1264 1342 1265 - // Display row can be either a revision row or a collapsed stack spacer 1343 + // Display row can be either a revision row or a collapsed stack row 1266 1344 type DisplayRow = 1267 1345 | { type: "revision"; row: GraphRow } 1268 - | { type: "collapsed-spacer"; stack: RevisionStack; lane: number }; 1346 + | { type: "collapsed-stack"; stack: RevisionStack; lane: number }; 1269 1347 1270 - // Filter rows to hide collapsed intermediate revisions and add spacers 1348 + // Filter rows to hide collapsed intermediate revisions and replace with a single collapsed stack row 1271 1349 const displayRows = useMemo(() => { 1272 1350 const result: DisplayRow[] = []; 1273 1351 ··· 1281 1359 // Stack is expanded - show the revision 1282 1360 result.push({ type: "revision", row }); 1283 1361 } 1284 - // If collapsed, skip this row (don't add it) 1362 + // If collapsed, skip this row 1285 1363 } else { 1286 1364 // Not an intermediate or not in a stack - always show 1287 1365 result.push({ type: "revision", row }); 1288 1366 1289 - // If this is the top of a collapsed stack, insert a spacer after it 1367 + // If this is the top of a collapsed stack, insert a collapsed stack row after it 1290 1368 if (stack && changeId === stack.topChangeId && !expandedStacks.has(stack.id)) { 1291 1369 const lane = changeIdToLane.get(changeId) ?? 0; 1292 - result.push({ type: "collapsed-spacer", stack, lane }); 1370 + result.push({ type: "collapsed-stack", stack, lane }); 1293 1371 } 1294 1372 } 1295 1373 } ··· 1305 1383 .map((d) => d.row), 1306 1384 [displayRows], 1307 1385 ); 1308 - 1309 1386 1310 1387 // Toggle stack expansion 1311 1388 function toggleStackExpansion(stackId: string) { ··· 1336 1413 commitToRowIndex.set(displayRow.row.revision.commit_id, i); 1337 1414 } 1338 1415 } 1339 - 1416 + 1340 1417 // Create a mapping of change_id -> commit_id for edge remapping 1341 1418 const changeIdToCommitId = new Map<string, string>(); 1342 1419 for (const rev of revisions) { ··· 1353 1430 const topCommitToStack = new Map<string, RevisionStack>(); 1354 1431 // Build mapping: commit_id -> stack (for edges within expanded stacks) 1355 1432 const commitToExpandedStack = new Map<string, RevisionStack>(); 1356 - 1433 + 1357 1434 for (const stack of stacks) { 1358 1435 if (!expandedStacks.has(stack.id)) { 1359 1436 // Stack is collapsed - map all intermediates to bottom revision 1360 1437 const bottomCommitId = changeIdToCommitId.get(stack.bottomChangeId); 1361 1438 const topCommitId = changeIdToCommitId.get(stack.topChangeId); 1362 - 1439 + 1363 1440 if (bottomCommitId && topCommitId) { 1364 1441 topCommitToStack.set(topCommitId, stack); 1365 - 1442 + 1366 1443 for (const intermediateChangeId of stack.intermediateChangeIds) { 1367 1444 const intermediateCommitId = changeIdToCommitId.get(intermediateChangeId); 1368 1445 if (intermediateCommitId) { 1369 - hiddenToVisible.set(intermediateCommitId, { targetCommitId: bottomCommitId, stack }); 1446 + hiddenToVisible.set(intermediateCommitId, { 1447 + targetCommitId: bottomCommitId, 1448 + stack, 1449 + }); 1370 1450 } 1371 1451 } 1372 1452 } ··· 1390 1470 let collapsedStackId: string | undefined; 1391 1471 let collapsedCount: number | undefined; 1392 1472 let expandedStackId: string | undefined; 1393 - 1473 + 1394 1474 // Check if this edge originates from a collapsed stack top 1395 1475 const stackFromTop = topCommitToStack.get(binding.sourceRevisionId); 1396 1476 if (stackFromTop && hiddenToVisible.has(targetId)) { ··· 1437 1517 const debugEnabledRef = useRef(debugEnabled); 1438 1518 debugEnabledRef.current = debugEnabled; 1439 1519 1520 + // Ref to hold scroll function - only scrolls if item is outside visible range 1521 + const scrollToIndexIfNeededRef = useRef<((index: number) => void) | null>(null); 1522 + 1440 1523 // Determine if selected revision is expanded based on URL search params 1441 1524 const isSelectedExpanded = expanded === true && !!selectedRevision; 1442 1525 ··· 1482 1565 }, 1483 1566 }); 1484 1567 1568 + // Helper to extend selection in a direction (macOS-style anchor-based selection) 1569 + const extendSelection = (direction: "down" | "up") => { 1570 + if (!selectedRevision) return; 1571 + const currentIndex = changeIdToIndex.get(selectedRevision.change_id); 1572 + if (currentIndex === undefined) return; 1573 + 1574 + const step = direction === "down" ? 1 : -1; 1575 + const limit = direction === "down" ? displayRows.length : -1; 1576 + 1577 + // Find the next revision in the given direction 1578 + let targetChangeId: string | null = null; 1579 + let targetIndex: number | null = null; 1580 + for (let i = currentIndex + step; direction === "down" ? i < limit : i > limit; i += step) { 1581 + const row = displayRows[i]; 1582 + if (row.type === "revision") { 1583 + targetChangeId = row.row.revision.change_id; 1584 + targetIndex = i; 1585 + break; 1586 + } 1587 + } 1588 + 1589 + if (!targetChangeId || targetIndex === null) return; 1590 + 1591 + // Determine anchor: use existing anchor or set it to current position 1592 + const anchorChangeId = selectionAnchor ?? selectedRevision.change_id; 1593 + const anchorIndex = changeIdToIndex.get(anchorChangeId); 1594 + if (anchorIndex === undefined) return; 1595 + 1596 + // Select all revisions between anchor and target (inclusive) 1597 + const startIndex = Math.min(anchorIndex, targetIndex); 1598 + const endIndex = Math.max(anchorIndex, targetIndex); 1599 + const newSelection = new Set<string>(); 1600 + for (let i = startIndex; i <= endIndex; i++) { 1601 + const row = displayRows[i]; 1602 + if (row.type === "revision") { 1603 + newSelection.add(row.row.revision.change_id); 1604 + } 1605 + } 1606 + 1607 + const selected = [...newSelection].join(","); 1608 + // Update URL with new selection, anchor, and move focus 1609 + navigate({ 1610 + search: { 1611 + ...search, 1612 + selected, 1613 + selectionAnchor: anchorChangeId, 1614 + rev: targetChangeId, 1615 + stack: undefined, 1616 + } as any, 1617 + replace: true, 1618 + }); 1619 + 1620 + // Scroll to keep item visible 1621 + scrollToIndexIfNeededRef.current?.(targetIndex); 1622 + }; 1623 + 1624 + // Shift+j: extend selection downward 1625 + useKeyboardShortcut({ 1626 + key: "j", 1627 + modifiers: { shift: true }, 1628 + onPress: () => extendSelection("down"), 1629 + enabled: !!selectedRevision && !inlineJumpMode, 1630 + }); 1631 + 1632 + // Shift+k: extend selection upward 1633 + useKeyboardShortcut({ 1634 + key: "k", 1635 + modifiers: { shift: true }, 1636 + onPress: () => extendSelection("up"), 1637 + enabled: !!selectedRevision && !inlineJumpMode, 1638 + }); 1639 + 1640 + // Shift+ArrowDown: extend selection downward 1641 + useKeyboardShortcut({ 1642 + key: "ArrowDown", 1643 + modifiers: { shift: true }, 1644 + onPress: () => extendSelection("down"), 1645 + enabled: !!selectedRevision && !inlineJumpMode, 1646 + }); 1647 + 1648 + // Shift+ArrowUp: extend selection upward 1649 + useKeyboardShortcut({ 1650 + key: "ArrowUp", 1651 + modifiers: { shift: true }, 1652 + onPress: () => extendSelection("up"), 1653 + enabled: !!selectedRevision && !inlineJumpMode, 1654 + }); 1655 + 1656 + // Get current focused index in displayRows (either revision or collapsed stack) 1657 + const getCurrentDisplayIndex = (): number => { 1658 + if (focusedStackId) { 1659 + return displayRows.findIndex( 1660 + (row) => row.type === "collapsed-stack" && row.stack.id === focusedStackId, 1661 + ); 1662 + } 1663 + if (selectedRevision) { 1664 + return displayRows.findIndex( 1665 + (row) => 1666 + row.type === "revision" && row.row.revision.change_id === selectedRevision.change_id, 1667 + ); 1668 + } 1669 + return -1; 1670 + }; 1671 + 1672 + // Navigate to a display row (revision or collapsed stack) 1673 + // Clears selection and anchor (regular navigation without shift) 1674 + const navigateToDisplayRow = (index: number) => { 1675 + const row = displayRows[index]; 1676 + if (!row) return; 1677 + 1678 + if (row.type === "revision") { 1679 + // Clear stack focus, selection, anchor and set revision 1680 + navigate({ 1681 + search: { 1682 + ...search, 1683 + stack: undefined, 1684 + rev: row.row.revision.change_id, 1685 + selected: undefined, 1686 + selectionAnchor: undefined, 1687 + } as any, 1688 + replace: true, 1689 + }); 1690 + } else if (row.type === "collapsed-stack") { 1691 + // Set stack focus and clear rev, selection, anchor 1692 + navigate({ 1693 + search: { 1694 + ...search, 1695 + stack: row.stack.id, 1696 + rev: undefined, 1697 + selected: undefined, 1698 + selectionAnchor: undefined, 1699 + } as any, 1700 + replace: true, 1701 + }); 1702 + } 1703 + 1704 + // Scroll to keep item visible (only if outside viewport) 1705 + scrollToIndexIfNeededRef.current?.(index); 1706 + }; 1707 + 1708 + // j / ArrowDown: navigate to next display row 1709 + useKeyboardShortcut({ 1710 + key: "j", 1711 + modifiers: {}, 1712 + onPress: () => { 1713 + const currentIndex = getCurrentDisplayIndex(); 1714 + if (currentIndex < 0) { 1715 + // No current focus, start from first 1716 + if (displayRows.length > 0) navigateToDisplayRow(0); 1717 + } else if (currentIndex < displayRows.length - 1) { 1718 + navigateToDisplayRow(currentIndex + 1); 1719 + } 1720 + }, 1721 + enabled: !inlineJumpMode, 1722 + }); 1723 + 1724 + useKeyboardShortcut({ 1725 + key: "ArrowDown", 1726 + modifiers: {}, 1727 + onPress: () => { 1728 + const currentIndex = getCurrentDisplayIndex(); 1729 + if (currentIndex < 0) { 1730 + if (displayRows.length > 0) navigateToDisplayRow(0); 1731 + } else if (currentIndex < displayRows.length - 1) { 1732 + navigateToDisplayRow(currentIndex + 1); 1733 + } 1734 + }, 1735 + enabled: !inlineJumpMode, 1736 + }); 1737 + 1738 + // k / ArrowUp: navigate to previous display row 1739 + useKeyboardShortcut({ 1740 + key: "k", 1741 + modifiers: {}, 1742 + onPress: () => { 1743 + const currentIndex = getCurrentDisplayIndex(); 1744 + if (currentIndex > 0) { 1745 + navigateToDisplayRow(currentIndex - 1); 1746 + } 1747 + }, 1748 + enabled: !inlineJumpMode, 1749 + }); 1750 + 1751 + useKeyboardShortcut({ 1752 + key: "ArrowUp", 1753 + modifiers: {}, 1754 + onPress: () => { 1755 + const currentIndex = getCurrentDisplayIndex(); 1756 + if (currentIndex > 0) { 1757 + navigateToDisplayRow(currentIndex - 1); 1758 + } 1759 + }, 1760 + enabled: !inlineJumpMode, 1761 + }); 1762 + 1763 + // Space/Enter on collapsed stack: expand it 1764 + useKeyboardShortcut({ 1765 + key: " ", 1766 + modifiers: {}, 1767 + onPress: () => { 1768 + if (focusedStackId) { 1769 + toggleStackExpansion(focusedStackId); 1770 + setFocusedStackId(null); 1771 + } else if (selectedRevision) { 1772 + toggleRevisionCheck(selectedRevision.change_id); 1773 + } 1774 + }, 1775 + enabled: !inlineJumpMode, 1776 + }); 1777 + 1778 + useKeyboardShortcut({ 1779 + key: "Enter", 1780 + modifiers: {}, 1781 + onPress: () => { 1782 + if (focusedStackId) { 1783 + toggleStackExpansion(focusedStackId); 1784 + setFocusedStackId(null); 1785 + } 1786 + }, 1787 + enabled: !!focusedStackId && !inlineJumpMode, 1788 + }); 1789 + 1485 1790 // Track if we just activated jump mode to ignore the same 'f' keypress 1486 1791 const justActivatedRef = useRef(false); 1487 1792 ··· 1513 1818 getScrollElement: () => parentRef.current, 1514 1819 estimateSize: (index: number) => { 1515 1820 const displayRow = displayRows[index]; 1516 - if (displayRow.type === "collapsed-spacer") { 1517 - // Fixed height spacer for collapsed stacks 1518 - return COLLAPSED_INDICATOR_HEIGHT; 1821 + if (displayRow.type === "collapsed-stack") { 1822 + // Same height as a regular revision row 1823 + return ROW_HEIGHT; 1519 1824 } 1520 1825 const row = displayRow.row; 1521 1826 const isExpanded = ··· 1525 1830 overscan: 10, 1526 1831 debug: debugEnabled, 1527 1832 }); 1833 + 1834 + // scrollToIndexIfNeededRef is updated below after virtualItems is computed 1528 1835 1529 1836 // Expose scrollToChangeId method via ref 1530 1837 useImperativeHandle(ref, () => ({ ··· 1595 1902 }, 1596 1903 })); 1597 1904 1598 - function handleSelect(changeId: string) { 1905 + function handleSelect(changeId: string, modifiers: { shift: boolean; meta: boolean }) { 1599 1906 const revision = revisionMapByChangeId.get(changeId); 1600 - if (revision) onSelectRevision(revision); 1907 + if (!revision) return; 1908 + 1909 + // Cmd/Ctrl+click: toggle selection 1910 + if (modifiers.meta) { 1911 + toggleRevisionCheck(changeId); 1912 + return; 1913 + } 1914 + 1915 + // Shift+click: range select from focused to clicked 1916 + if (modifiers.shift && selectedRevision) { 1917 + const focusedIndex = changeIdToIndex.get(selectedRevision.change_id); 1918 + const clickedIndex = changeIdToIndex.get(changeId); 1919 + if (focusedIndex !== undefined && clickedIndex !== undefined) { 1920 + const startIdx = Math.min(focusedIndex, clickedIndex); 1921 + const endIdx = Math.max(focusedIndex, clickedIndex); 1922 + const newSelection = new Set<string>(); 1923 + for (let i = startIdx; i <= endIdx; i++) { 1924 + const displayRow = displayRows[i]; 1925 + if (displayRow.type === "revision") { 1926 + newSelection.add(displayRow.row.revision.change_id); 1927 + } 1928 + } 1929 + // Update selection in URL 1930 + const selected = newSelection.size > 0 ? [...newSelection].join(",") : undefined; 1931 + navigate({ 1932 + search: { ...search, selected, stack: undefined } as any, 1933 + replace: true, 1934 + }); 1935 + } 1936 + return; 1937 + } 1938 + 1939 + // Plain click: focus revision (clear selection, anchor, and stack focus) 1940 + navigate({ 1941 + search: { 1942 + ...search, 1943 + selected: undefined, 1944 + selectionAnchor: undefined, 1945 + stack: undefined, 1946 + rev: changeId, 1947 + } as any, 1948 + replace: true, 1949 + }); 1601 1950 } 1602 1951 1603 1952 const virtualItems = rowVirtualizer.getVirtualItems(); 1604 1953 const visibleStartRow = virtualItems[0]?.index ?? 0; 1605 1954 const visibleEndRow = virtualItems[virtualItems.length - 1]?.index ?? 0; 1606 1955 const totalHeight = rowVirtualizer.getTotalSize(); 1956 + 1957 + // Update scroll ref - compute actually visible range based on scroll position 1958 + scrollToIndexIfNeededRef.current = (index: number) => { 1959 + const scrollEl = parentRef.current; 1960 + if (!scrollEl) return; 1961 + 1962 + const scrollTop = scrollEl.scrollTop; 1963 + const clientHeight = scrollEl.clientHeight; 1964 + 1965 + // Calculate which rows are fully visible (not just rendered with overscan) 1966 + // Use ceil for start (first fully visible) and floor-1 for end (last fully visible) 1967 + const visibleStart = Math.ceil(scrollTop / ROW_HEIGHT); 1968 + const visibleEnd = Math.floor((scrollTop + clientHeight) / ROW_HEIGHT) - 1; 1969 + 1970 + const shouldScroll = index < visibleStart || index > visibleEnd; 1971 + 1972 + // Only scroll if the item is outside the fully visible range 1973 + if (shouldScroll) { 1974 + rowVirtualizer.scrollToIndex(index, { align: "auto" }); 1975 + } 1976 + }; 1607 1977 const rowOffsets = new Map<number, number>(); 1608 1978 for (const item of virtualItems) { 1609 1979 rowOffsets.set(item.index, item.start); ··· 1729 2099 1730 2100 window.addEventListener("keydown", handleJumpKey); 1731 2101 return () => window.removeEventListener("keydown", handleJumpKey); 1732 - }, [inlineJumpMode, inlineJumpQuery, setInlineJumpQuery, revisionMapByChangeId, onSelectRevision]); 2102 + }, [ 2103 + inlineJumpMode, 2104 + inlineJumpQuery, 2105 + setInlineJumpQuery, 2106 + revisionMapByChangeId, 2107 + onSelectRevision, 2108 + ]); 1733 2109 1734 2110 if (revisions.length === 0) { 1735 2111 return ( ··· 1786 2162 {virtualItems.map((virtualRow) => { 1787 2163 const displayRow = displayRows[virtualRow.index]; 1788 2164 1789 - // Collapsed stack spacer row - button positioned at edge midpoint 1790 - if (displayRow.type === "collapsed-spacer") { 2165 + // Collapsed stack row - styled as stacked cards 2166 + if (displayRow.type === "collapsed-stack") { 1791 2167 const { stack, lane } = displayRow; 1792 - // Get the row indices for top and bottom of this stack 1793 - const topRowIdx = changeIdToIndex.get(stack.topChangeId); 1794 - const bottomRowIdx = changeIdToIndex.get(stack.bottomChangeId); 1795 - // Calculate button position at midpoint of dotted edge 1796 - const topCenter = topRowIdx !== undefined ? getRowCenter(topRowIdx) : virtualRow.start; 1797 - const bottomCenter = bottomRowIdx !== undefined ? getRowCenter(bottomRowIdx) : virtualRow.start + COLLAPSED_INDICATOR_HEIGHT; 1798 - const edgeMidY = (topCenter + bottomCenter) / 2; 1799 - // Position button relative to spacer row start 1800 - const buttonOffsetY = edgeMidY - virtualRow.start - 12; // 12 = half button height 1801 - 2168 + const nodeAreaWidth = LANE_PADDING + (lane + 1) * LANE_WIDTH; 2169 + const count = stack.intermediateChangeIds.length; 2170 + // Show up to 3 stacked card layers 2171 + const layers = Math.min(count, 3); 2172 + 2173 + // Check if this stack is related to the selected revision (for dimming) 2174 + const isStackRelated = stack.changeIds.some((id) => relatedRevisions.has(id)); 2175 + const isStackDimmed = selectedRevision !== null && !isStackRelated; 2176 + const isStackFocused = focusedStackId === stack.id; 2177 + 1802 2178 return ( 1803 2179 <div 1804 - key={`spacer-${stack.id}`} 2180 + key={`collapsed-${stack.id}`} 1805 2181 ref={rowVirtualizer.measureElement} 1806 2182 data-index={virtualRow.index} 1807 - className="absolute left-0 w-full pointer-events-none" 2183 + className="absolute left-0 w-full" 1808 2184 style={{ 1809 2185 transform: `translateY(${virtualRow.start}px)`, 1810 - height: COLLAPSED_INDICATOR_HEIGHT, 2186 + height: ROW_HEIGHT, 1811 2187 }} 1812 2188 > 1813 - <button 1814 - type="button" 1815 - onClick={() => toggleStackExpansion(stack.id)} 1816 - className="absolute flex items-center gap-1.5 px-3 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 transition-colors pointer-events-auto rounded" 1817 - style={{ 1818 - top: buttonOffsetY + 4, 1819 - left: (lane + 1) * LANE_WIDTH + 16, 1820 - backgroundColor: "transparent", 1821 - }} 1822 - > 1823 - <svg 1824 - className="w-3 h-3" 1825 - fill="none" 1826 - viewBox="0 0 24 24" 1827 - stroke="currentColor" 1828 - > 1829 - <path 1830 - strokeLinecap="round" 1831 - strokeLinejoin="round" 1832 - strokeWidth={2} 1833 - d="M19 9l-7 7-7-7" 1834 - /> 1835 - </svg> 1836 - <span> 1837 - {stack.intermediateChangeIds.length} hidden revision 1838 - {stack.intermediateChangeIds.length !== 1 ? "s" : ""} 1839 - </span> 1840 - </button> 2189 + <div className="flex flex-col relative" style={{ height: ROW_HEIGHT }}> 2190 + <div className="flex items-start min-h-[56px] pt-4"> 2191 + {/* Spacer for graph area */} 2192 + <div className="shrink-0" style={{ width: nodeAreaWidth }} /> 2193 + <button 2194 + type="button" 2195 + onClick={() => toggleStackExpansion(stack.id)} 2196 + className={`relative flex-1 mr-2 min-w-0 my-2 mx-1 cursor-pointer group ${isStackDimmed ? "opacity-40" : ""}`} 2197 + style={{ height: 40 }} 2198 + > 2199 + {/* Stacked card layers */} 2200 + {Array.from({ length: layers }).map((_, i) => { 2201 + const layerIndex = layers - 1 - i; // Render back layers first 2202 + const offset = layerIndex * 4; 2203 + const isTopLayer = layerIndex === 0; 2204 + const scale = 1 - layerIndex * 0.02; 2205 + 2206 + return ( 2207 + <div 2208 + key={layerIndex} 2209 + className={`absolute left-0 right-0 rounded border shadow-sm group-hover:border-muted-foreground/50 ${ 2210 + isStackFocused && isTopLayer 2211 + ? "bg-accent/40 border-accent/60" 2212 + : "bg-card border-border" 2213 + } text-card-foreground`} 2214 + style={{ 2215 + top: 0, 2216 + height: 40, 2217 + transform: `translateY(${offset}px) scaleX(${scale})`, 2218 + transformOrigin: "top center", 2219 + opacity: 1 - layerIndex * 0.2, 2220 + zIndex: layers - layerIndex, 2221 + }} 2222 + /> 2223 + ); 2224 + })} 2225 + {/* Content overlay on top card */} 2226 + <div 2227 + className="absolute inset-0 flex items-center justify-center gap-2 rounded" 2228 + style={{ zIndex: layers + 1, height: 40 }} 2229 + > 2230 + <svg 2231 + className="w-3.5 h-3.5 text-muted-foreground" 2232 + fill="none" 2233 + viewBox="0 0 24 24" 2234 + stroke="currentColor" 2235 + > 2236 + <path 2237 + strokeLinecap="round" 2238 + strokeLinejoin="round" 2239 + strokeWidth={2} 2240 + d="M19 9l-7 7-7-7" 2241 + /> 2242 + </svg> 2243 + <span className="text-xs text-muted-foreground group-hover:text-foreground"> 2244 + {count} hidden revision{count !== 1 ? "s" : ""} 2245 + </span> 2246 + </div> 2247 + </button> 2248 + </div> 2249 + </div> 1841 2250 </div> 1842 2251 ); 1843 2252 } 1844 2253 1845 2254 // Regular revision row 1846 - const row = displayRow.row; 2255 + const { row } = displayRow; 1847 2256 const lane = changeIdToLane.get(row.revision.change_id) ?? 0; 1848 2257 const isFlashing = flash?.changeId === row.revision.change_id; 1849 2258 const isDimmed = 1850 2259 selectedRevision !== null && !relatedRevisions.has(row.revision.change_id); 1851 - const isFocused = selectedRevision?.change_id === row.revision.change_id; 2260 + // Only show focus if no stack is focused 2261 + const isFocused = 2262 + !focusedStackId && selectedRevision?.change_id === row.revision.change_id; 1852 2263 const isSelected = isFocused; 1853 2264 const isExpanded = isSelectedExpanded && isFocused; 1854 2265 ··· 1862 2273 transform: `translateY(${virtualRow.start}px)`, 1863 2274 }} 1864 2275 > 1865 - <RevisionRow 1866 - revision={row.revision} 1867 - lane={lane} 1868 - maxLaneOnRow={row.maxLaneOnRow} 1869 - isSelected={isSelected} 1870 - isFocused={isFocused} 1871 - onSelect={handleSelect} 1872 - isFlashing={isFlashing} 1873 - isDimmed={isDimmed} 1874 - isExpanded={isExpanded} 1875 - repoPath={repoPath} 1876 - isPendingAbandon={pendingAbandon?.change_id === row.revision.change_id} 1877 - jumpHint={jumpHintsMap.get(row.revision.change_id) ?? null} 1878 - jumpModeActive={inlineJumpMode} 1879 - jumpQuery={inlineJumpQuery ?? ""} 1880 - /> 2276 + <RevisionRow 2277 + revision={row.revision} 2278 + lane={lane} 2279 + maxLaneOnRow={row.maxLaneOnRow} 2280 + isSelected={isSelected} 2281 + isChecked={selectedRevisions.has(row.revision.change_id)} 2282 + isFocused={isFocused} 2283 + onSelect={handleSelect} 2284 + isFlashing={isFlashing} 2285 + isDimmed={isDimmed} 2286 + isExpanded={isExpanded} 2287 + repoPath={repoPath} 2288 + isPendingAbandon={pendingAbandon?.change_id === row.revision.change_id} 2289 + jumpHint={jumpHintsMap.get(row.revision.change_id) ?? null} 2290 + jumpModeActive={inlineJumpMode} 2291 + jumpQuery={inlineJumpQuery ?? ""} 2292 + /> 1881 2293 </div> 1882 2294 ); 1883 2295 })}
+17 -20
apps/desktop/src/components/revision-graph-utils.ts
··· 33 33 } 34 34 35 35 const commitIds = new Set(revisions.map((r) => r.commit_id)); 36 - 36 + 37 37 // Build direct parent/child relationships (only within visible revset) 38 38 const parents = new Map<string, string[]>(); 39 39 const children = new Map<string, string[]>(); 40 - 40 + 41 41 for (const rev of revisions) { 42 42 const visibleParents: string[] = []; 43 43 for (const edge of rev.parent_edges) { ··· 45 45 if (edge.edge_type === "missing") continue; 46 46 if (!commitIds.has(edge.parent_id)) continue; 47 47 visibleParents.push(edge.parent_id); 48 - 48 + 49 49 // Build children map 50 50 const parentChildren = children.get(edge.parent_id) ?? []; 51 51 parentChildren.push(rev.commit_id); ··· 53 53 } 54 54 parents.set(rev.commit_id, visibleParents); 55 55 } 56 - 56 + 57 57 // Ensure all commits have entries even if they have no children/parents 58 58 for (const rev of revisions) { 59 59 if (!children.has(rev.commit_id)) { ··· 66 66 for (const rev of revisions) { 67 67 const ancestorSet = new Set<string>(); 68 68 const queue = [...(parents.get(rev.commit_id) ?? [])]; 69 - 69 + 70 70 while (queue.length > 0) { 71 71 const parentId = queue.shift()!; 72 72 if (ancestorSet.has(parentId)) continue; 73 73 ancestorSet.add(parentId); 74 74 queue.push(...(parents.get(parentId) ?? [])); 75 75 } 76 - 76 + 77 77 ancestors.set(rev.commit_id, ancestorSet); 78 78 } 79 79 ··· 82 82 for (const rev of revisions) { 83 83 const descendantSet = new Set<string>(); 84 84 const queue = [...(children.get(rev.commit_id) ?? [])]; 85 - 85 + 86 86 while (queue.length > 0) { 87 87 const childId = queue.shift()!; 88 88 if (descendantSet.has(childId)) continue; 89 89 descendantSet.add(childId); 90 90 queue.push(...(children.get(childId) ?? [])); 91 91 } 92 - 92 + 93 93 descendants.set(rev.commit_id, descendantSet); 94 94 } 95 95 ··· 122 122 ): Map<string, string[]> { 123 123 const components = new Map<string, string[]>(); // componentId -> commit_ids 124 124 const commitToComponent = new Map<string, string>(); 125 - 125 + 126 126 for (const rev of revisions) { 127 127 if (commitToComponent.has(rev.commit_id)) continue; 128 - 128 + 129 129 // Start a new component with this revision as the root 130 130 const componentId = rev.commit_id; 131 131 const componentMembers: string[] = []; 132 132 const queue = [rev.commit_id]; 133 - 133 + 134 134 while (queue.length > 0) { 135 135 const commitId = queue.shift()!; 136 136 if (commitToComponent.has(commitId)) continue; 137 - 137 + 138 138 commitToComponent.set(commitId, componentId); 139 139 componentMembers.push(commitId); 140 - 140 + 141 141 // Add all ancestors and descendants to the component 142 142 const ancestorSet = ancestry.ancestors.get(commitId) ?? new Set(); 143 143 const descendantSet = ancestry.descendants.get(commitId) ?? new Set(); 144 - 144 + 145 145 for (const ancestorId of ancestorSet) { 146 146 if (!commitToComponent.has(ancestorId)) { 147 147 queue.push(ancestorId); ··· 153 153 } 154 154 } 155 155 } 156 - 156 + 157 157 components.set(componentId, componentMembers); 158 158 } 159 - 159 + 160 160 return components; 161 161 } 162 162 ··· 311 311 return stacks; 312 312 } 313 313 314 - export function reorderForGraph( 315 - revisions: Revision[], 316 - recency?: CommitRecency, 317 - ): Revision[] { 314 + export function reorderForGraph(revisions: Revision[], recency?: CommitRecency): Revision[] { 318 315 if (revisions.length === 0) return []; 319 316 320 317 const commitMap = new Map(revisions.map((r) => [r.commit_id, r]));
+38
apps/desktop/src/components/ui/checkbox.tsx
··· 1 + import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; 2 + import { CheckIcon } from "lucide-react"; 3 + 4 + import { cn } from "@/lib/utils"; 5 + 6 + interface CheckboxProps extends Omit<CheckboxPrimitive.Root.Props, "render"> { 7 + /** Show indicator only on hover when unchecked */ 8 + showOnHover?: boolean; 9 + } 10 + 11 + function Checkbox({ className, showOnHover = false, ...props }: CheckboxProps) { 12 + return ( 13 + <CheckboxPrimitive.Root 14 + className={cn( 15 + "group/checkbox peer shrink-0 rounded-sm border transition-colors", 16 + "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", 17 + "disabled:cursor-not-allowed disabled:opacity-50", 18 + "data-[checked]:bg-primary data-[checked]:border-primary data-[checked]:text-primary-foreground", 19 + "data-[unchecked]:border-transparent data-[unchecked]:hover:border-muted-foreground/40", 20 + className, 21 + )} 22 + {...props} 23 + > 24 + <CheckboxPrimitive.Indicator 25 + className={cn( 26 + "flex items-center justify-center text-current transition-opacity size-full", 27 + showOnHover 28 + ? "opacity-0 group-hover/checkbox:opacity-40 group-data-[checked]/checkbox:opacity-100" 29 + : "opacity-0 group-data-[checked]/checkbox:opacity-100", 30 + )} 31 + > 32 + <CheckIcon className="size-full" /> 33 + </CheckboxPrimitive.Indicator> 34 + </CheckboxPrimitive.Root> 35 + ); 36 + } 37 + 38 + export { Checkbox };
+7 -5
apps/desktop/src/components/ui/command.tsx
··· 50 50 <DialogTitle>{title}</DialogTitle> 51 51 <DialogDescription>{description}</DialogDescription> 52 52 </DialogHeader> 53 - <DialogContent 54 - className={cn("rounded-lg overflow-hidden p-0 top-[20%] translate-y-0", className)} 55 - showCloseButton={showCloseButton} 56 - > 57 - <Command filter={filter} shouldFilter={shouldFilter}>{children}</Command> 53 + <DialogContent 54 + className={cn("rounded-lg overflow-hidden p-0 top-[20%] translate-y-0", className)} 55 + showCloseButton={showCloseButton} 56 + > 57 + <Command filter={filter} shouldFilter={shouldFilter}> 58 + {children} 59 + </Command> 58 60 </DialogContent> 59 61 </Dialog> 60 62 );
+221 -92
apps/desktop/src/db.ts
··· 4 4 import { listen } from "@tauri-apps/api/event"; 5 5 import type { ChangedFile, Repository, Revision } from "@/tauri-commands"; 6 6 import { 7 + getCommitRecency, 7 8 getRepositories, 8 9 getRevisionChanges, 9 10 getRevisionDiff, ··· 11 12 jjAbandon, 12 13 jjEdit, 13 14 jjNew, 15 + removeRepository, 16 + upsertRepository, 14 17 watchRepository, 15 18 } from "@/tauri-commands"; 16 19 ··· 18 21 // Query Client (shared by all collections) 19 22 // ============================================================================ 20 23 21 - export const queryClient = new QueryClient(); 24 + export const queryClient = new QueryClient({ 25 + defaultOptions: { 26 + queries: { 27 + staleTime: Number.POSITIVE_INFINITY, // Data fresh until watcher invalidates 28 + gcTime: 5 * 60 * 1000, // 5 minutes 29 + refetchOnWindowFocus: false, // Watcher handles this 30 + refetchOnMount: false, // Already have data from watcher 31 + }, 32 + }, 33 + }); 34 + 35 + // ============================================================================ 36 + // In-flight Mutation Tracking 37 + // ============================================================================ 38 + 39 + const inFlightMutations = new Set<string>(); 40 + 41 + function trackMutation<T>(mutationId: string, promise: Promise<T>): Promise<T> { 42 + inFlightMutations.add(mutationId); 43 + return promise.finally(() => { 44 + inFlightMutations.delete(mutationId); 45 + }); 46 + } 47 + 48 + // ============================================================================ 49 + // Shared Repository Watcher (one per repo, invalidates all queries) 50 + // ============================================================================ 51 + 52 + const repoWatchers = new Map<string, { unlisten: () => void; refCount: number }>(); 53 + 54 + async function setupRepoWatcher(repoPath: string): Promise<void> { 55 + const existing = repoWatchers.get(repoPath); 56 + if (existing) { 57 + existing.refCount++; 58 + return; 59 + } 60 + 61 + await watchRepository(repoPath); 62 + const unlisten = await listen<string>("repo-changed", async (event) => { 63 + if (event.payload === repoPath) { 64 + // Skip if there are in-flight mutations - let the mutation handle state 65 + if (inFlightMutations.size > 0) { 66 + console.log("[watcher] skipping - in-flight mutations:", [...inFlightMutations]); 67 + return; 68 + } 69 + 70 + console.log("[watcher] invalidating queries for:", repoPath); 71 + // Invalidate ALL queries for this repo - TanStack Query will refetch 72 + await queryClient.invalidateQueries({ queryKey: ["revisions", repoPath] }); 73 + await queryClient.invalidateQueries({ queryKey: ["revision-changes", repoPath] }); 74 + await queryClient.invalidateQueries({ queryKey: ["revision-diff", repoPath] }); 75 + await queryClient.invalidateQueries({ queryKey: ["commit-recency", repoPath] }); 76 + } 77 + }); 78 + 79 + repoWatchers.set(repoPath, { unlisten, refCount: 1 }); 80 + } 22 81 23 82 // ============================================================================ 24 83 // Repositories Collection ··· 33 92 }), 34 93 }); 35 94 95 + export type RepositoriesCollection = typeof repositoriesCollection; 96 + 97 + export async function addRepository(collection: RepositoriesCollection, repository: Repository) { 98 + // Optimistic update first 99 + collection.utils.writeUpsert([repository]); 100 + 101 + try { 102 + await upsertRepository(repository); 103 + console.log("[addRepository] upsertRepository completed"); 104 + } catch (err) { 105 + // Revert on failure 106 + collection.utils.writeDelete(repository.id); 107 + console.error("[addRepository] failed, reverting:", err); 108 + throw err; 109 + } 110 + } 111 + 112 + export async function updateRepository(collection: RepositoriesCollection, repository: Repository) { 113 + // Get current state for potential revert 114 + const current = collection.state.get(repository.id); 115 + 116 + // Optimistic update 117 + collection.utils.writeUpsert([repository]); 118 + 119 + try { 120 + await upsertRepository(repository); 121 + console.log("[updateRepository] upsertRepository completed"); 122 + } catch (err) { 123 + // Revert on failure 124 + if (current) { 125 + collection.utils.writeUpsert([current]); 126 + } else { 127 + collection.utils.writeDelete(repository.id); 128 + } 129 + console.error("[updateRepository] failed, reverting:", err); 130 + throw err; 131 + } 132 + } 133 + 134 + export async function deleteRepository(collection: RepositoriesCollection, repositoryId: string) { 135 + // Get current state for potential revert 136 + const current = collection.state.get(repositoryId); 137 + 138 + // Optimistic delete 139 + collection.utils.writeDelete(repositoryId); 140 + 141 + try { 142 + await removeRepository(repositoryId); 143 + console.log("[deleteRepository] removeRepository completed"); 144 + } catch (err) { 145 + // Revert on failure 146 + if (current) { 147 + collection.utils.writeUpsert([current]); 148 + } 149 + console.error("[deleteRepository] failed, reverting:", err); 150 + throw err; 151 + } 152 + } 153 + 36 154 // ============================================================================ 37 155 // Revisions Collection 38 156 // ============================================================================ ··· 55 173 }); 56 174 57 175 const revisionCollections = new Map<string, ReturnType<typeof createRevisionsCollection>>(); 58 - const revisionWatchers = new Map<string, { unlisten: () => void; refCount: number }>(); 59 - 60 - // Track in-flight edit mutations to prevent watcher from overwriting optimistic state 61 - const inFlightEdits = new Set<string>(); 62 176 63 177 function createRevisionsCollection(repoPath: string, preset?: string, customRevset?: string) { 64 178 const limit = preset === "full_history" ? 10000 : 100; 65 - const collection = createCollection({ 179 + 180 + // Set up the shared watcher (idempotent - increments refCount if already exists) 181 + setupRepoWatcher(repoPath); 182 + 183 + return createCollection({ 66 184 ...queryCollectionOptions({ 67 185 queryClient, 68 186 queryKey: ["revisions", repoPath, preset, customRevset], ··· 70 188 getKey: getRevisionKey, 71 189 }), 72 190 }); 73 - 74 - // Set up file watcher with refcounting 75 - const setupWatcher = async () => { 76 - const existing = revisionWatchers.get(repoPath); 77 - if (existing) { 78 - existing.refCount++; 79 - return; 80 - } 81 - 82 - await watchRepository(repoPath); 83 - const unlisten = await listen<string>("repo-changed", async (event) => { 84 - if (event.payload === repoPath) { 85 - // Skip if there are in-flight edits - let the mutation handle state 86 - if (inFlightEdits.size > 0) { 87 - console.log("[watcher] skipping - in-flight edits:", [...inFlightEdits]); 88 - return; 89 - } 90 - 91 - console.log("[watcher] fetching revisions..."); 92 - const revisions = await getRevisions( 93 - repoPath, 94 - limit, 95 - customRevset, 96 - customRevset ? undefined : preset, 97 - ); 98 - 99 - // Debug: log divergent changes 100 - const divergentCount = revisions.filter((r) => r.is_divergent).length; 101 - if (divergentCount > 0) { 102 - console.log("[watcher] found", divergentCount, "divergent revisions"); 103 - } 104 - 105 - console.log("[watcher] got", revisions.length, "revisions, wc:", revisions.find(r => r.is_working_copy)?.change_id_short); 106 - 107 - // Delete revisions that are no longer in the result set 108 - const newKeys = new Set(revisions.map(getRevisionKey)); 109 - for (const key of collection.state.keys()) { 110 - if (!newKeys.has(key)) { 111 - collection.utils.writeDelete(key); 112 - } 113 - } 114 - 115 - collection.utils.writeUpsert(revisions); 116 - } 117 - }); 118 - 119 - revisionWatchers.set(repoPath, { unlisten, refCount: 1 }); 120 - }; 121 - 122 - setupWatcher(); 123 - 124 - return collection; 125 191 } 126 192 127 193 export type RevisionsCollection = ReturnType<typeof createRevisionsCollection>; ··· 142 208 targetRevision: Revision, 143 209 currentWcRevision: Revision | null, 144 210 ) { 145 - console.log("[editRevision] start, updating synced layer directly"); 211 + const mutationId = `edit-${Date.now()}-${Math.random()}`; 212 + console.log("[editRevision] start, mutationId:", mutationId); 146 213 147 - // Update synced layer directly (not optimistic) - this is instant 214 + // Optimistic update 148 215 const updates: Revision[] = []; 149 - 150 216 if (currentWcRevision && getRevisionKey(currentWcRevision) !== getRevisionKey(targetRevision)) { 151 217 updates.push({ ...currentWcRevision, is_working_copy: false }); 152 218 } 153 219 updates.push({ ...targetRevision, is_working_copy: true }); 154 - 155 220 collection.utils.writeUpsert(updates); 156 - console.log("[editRevision] synced layer updated, firing backend..."); 157 221 158 - // Fire backend in background - watcher will confirm/correct if needed 159 - // For divergent changes, use change_id_short which includes /N suffix 160 - jjEdit(repoPath, targetRevision.change_id_short) 161 - .then(() => console.log("[editRevision] jjEdit completed")) 222 + // Track the mutation and fire backend 223 + trackMutation(mutationId, jjEdit(repoPath, targetRevision.change_id_short)) 224 + .then(() => { 225 + console.log("[editRevision] completed"); 226 + // Invalidate to get fresh data from backend 227 + queryClient.invalidateQueries({ queryKey: ["revisions", repoPath] }); 228 + }) 162 229 .catch((err) => { 163 - console.error("[editRevision] jjEdit failed:", err); 164 - // Revert on failure 230 + console.error("[editRevision] failed:", err); 231 + // Revert optimistic update 165 232 const revertUpdates: Revision[] = []; 166 - if (currentWcRevision && getRevisionKey(currentWcRevision) !== getRevisionKey(targetRevision)) { 233 + if ( 234 + currentWcRevision && 235 + getRevisionKey(currentWcRevision) !== getRevisionKey(targetRevision) 236 + ) { 167 237 revertUpdates.push({ ...currentWcRevision, is_working_copy: true }); 168 238 } 169 239 revertUpdates.push({ ...targetRevision, is_working_copy: false }); ··· 172 242 } 173 243 174 244 export function newRevision(repoPath: string, parentChangeIds: string[]) { 245 + const mutationId = `new-${Date.now()}-${Math.random()}`; 246 + console.log("[newRevision] start, mutationId:", mutationId); 247 + 175 248 const tx = createTransaction({ 176 249 mutationFn: async () => { 177 - await jjNew(repoPath, parentChangeIds); 250 + await trackMutation(mutationId, jjNew(repoPath, parentChangeIds)); 251 + // Invalidate to get fresh data including the new revision 252 + await queryClient.invalidateQueries({ queryKey: ["revisions", repoPath] }); 178 253 }, 179 254 }); 180 255 181 256 tx.mutate(() => { 182 257 // No optimistic update - we don't know the new revision's ID 183 - // File watcher will add it to the collection 258 + // TanStack Query invalidation will add it to the collection 184 259 }); 185 260 186 261 return tx; ··· 190 265 collection: RevisionsCollection, 191 266 repoPath: string, 192 267 revision: Revision, 193 - limit: number, 194 - customRevset?: string, 195 - preset?: string, 268 + _limit: number, 269 + _customRevset?: string, 270 + _preset?: string, 196 271 ) { 197 - console.log("[abandonRevision] abandoning:", revision.change_id_short); 272 + const mutationId = `abandon-${Date.now()}-${Math.random()}`; 273 + console.log("[abandonRevision] abandoning:", revision.change_id_short, "mutationId:", mutationId); 198 274 199 275 // For working copy, jj creates a new WC - can't do optimistic delete 200 276 // For other revisions, we can optimistically remove ··· 202 278 collection.utils.writeDelete(getRevisionKey(revision)); 203 279 } 204 280 205 - // Fire backend and then refetch to get new state (especially for WC abandon which creates new WC) 206 - jjAbandon(repoPath, revision.change_id_short) 207 - .then(async () => { 208 - console.log("[abandonRevision] completed, refetching..."); 209 - // Refetch to get the new working copy if we abandoned WC 210 - const revisions = await getRevisions(repoPath, limit, customRevset, customRevset ? undefined : preset); 211 - const newKeys = new Set(revisions.map(getRevisionKey)); 212 - for (const key of collection.state.keys()) { 213 - if (!newKeys.has(key)) { 214 - collection.utils.writeDelete(key); 215 - } 216 - } 217 - collection.utils.writeUpsert(revisions); 281 + // Track the mutation and fire backend 282 + trackMutation(mutationId, jjAbandon(repoPath, revision.change_id_short)) 283 + .then(() => { 284 + console.log("[abandonRevision] completed"); 285 + // Invalidate to get fresh data (especially for WC abandon which creates new WC) 286 + queryClient.invalidateQueries({ queryKey: ["revisions", repoPath] }); 218 287 }) 219 288 .catch((err) => { 220 289 console.error("[abandonRevision] failed:", err); ··· 229 298 // Revision Changes Collections (ChangedFile[] per revision) 230 299 // ============================================================================ 231 300 232 - const revisionChangesCollections = new Map<string, ReturnType<typeof createRevisionChangesCollection>>(); 301 + const revisionChangesCollections = new Map< 302 + string, 303 + ReturnType<typeof createRevisionChangesCollection> 304 + >(); 233 305 234 306 function createRevisionChangesCollection(repoPath: string, changeId: string) { 235 307 return createCollection({ ··· 244 316 245 317 export type RevisionChangesCollection = ReturnType<typeof createRevisionChangesCollection>; 246 318 247 - export function getRevisionChangesCollection(repoPath: string, changeId: string): RevisionChangesCollection { 319 + export function getRevisionChangesCollection( 320 + repoPath: string, 321 + changeId: string, 322 + ): RevisionChangesCollection { 248 323 const cacheKey = `${repoPath}:${changeId}`; 249 324 let collection = revisionChangesCollections.get(cacheKey); 250 325 if (!collection) { ··· 291 366 292 367 export type RevisionDiffCollection = ReturnType<typeof createRevisionDiffCollection>; 293 368 294 - export function getRevisionDiffCollection(repoPath: string, changeId: string): RevisionDiffCollection { 369 + export function getRevisionDiffCollection( 370 + repoPath: string, 371 + changeId: string, 372 + ): RevisionDiffCollection { 295 373 const cacheKey = `${repoPath}:${changeId}`; 296 374 let collection = revisionDiffCollections.get(cacheKey); 297 375 if (!collection) { ··· 334 412 getRevisionChangesCollection(repoPath, changeId); 335 413 } 336 414 } 415 + 416 + // ============================================================================ 417 + // Commit Recency Collection (for branch ordering) 418 + // ============================================================================ 419 + 420 + // Wrapper type for commit recency data to work with collection pattern 421 + interface CommitRecencyEntry { 422 + id: "recency"; 423 + data: Record<string, number>; 424 + } 425 + 426 + const commitRecencyCollections = new Map< 427 + string, 428 + ReturnType<typeof createCommitRecencyCollection> 429 + >(); 430 + 431 + function createCommitRecencyCollection(repoPath: string) { 432 + return createCollection({ 433 + ...queryCollectionOptions({ 434 + queryClient, 435 + queryKey: ["commit-recency", repoPath], 436 + queryFn: async () => { 437 + const recency = await getCommitRecency(repoPath, 500); 438 + return [{ id: "recency" as const, data: recency }]; 439 + }, 440 + getKey: (entry: CommitRecencyEntry) => entry.id, 441 + staleTime: 30_000, // 30 seconds - this one uses time-based staleness 442 + }), 443 + }); 444 + } 445 + 446 + export type CommitRecencyCollection = ReturnType<typeof createCommitRecencyCollection>; 447 + 448 + export function getCommitRecencyCollection(repoPath: string): CommitRecencyCollection { 449 + const cacheKey = repoPath; 450 + let collection = commitRecencyCollections.get(cacheKey); 451 + if (!collection) { 452 + collection = createCommitRecencyCollection(repoPath); 453 + commitRecencyCollections.set(cacheKey, collection); 454 + } 455 + return collection; 456 + } 457 + 458 + export const emptyCommitRecencyCollection = createCollection({ 459 + ...queryCollectionOptions({ 460 + queryClient, 461 + queryKey: ["commit-recency", "empty"], 462 + queryFn: () => Promise.resolve([]), 463 + getKey: (entry: CommitRecencyEntry) => entry.id, 464 + }), 465 + });
+9 -4
apps/desktop/src/hooks/useKeyboard.ts
··· 11 11 selectedChangeId: string | null; 12 12 onNavigate: (changeId: string) => void; 13 13 scrollToChangeId?: (changeId: string, options?: ScrollOptions) => void; 14 + /** If true, j/k/arrow navigation is handled elsewhere (e.g., for display rows with collapsed stacks) */ 15 + disableBasicNavigation?: boolean; 14 16 } 15 17 16 18 interface UseKeyboardShortcutOptions { ··· 40 42 selectedChangeId, 41 43 onNavigate, 42 44 scrollToChangeId, 45 + disableBasicNavigation = false, 43 46 }: UseKeyboardNavigationOptions) { 44 47 // Use refs to avoid stale closures in event handler 45 48 const orderedRevisionsRef = useRef(orderedRevisions); 46 49 const selectedChangeIdRef = useRef(selectedChangeId); 47 50 const onNavigateRef = useRef(onNavigate); 48 51 const scrollToChangeIdRef = useRef(scrollToChangeId); 52 + const disableBasicNavigationRef = useRef(disableBasicNavigation); 49 53 50 54 orderedRevisionsRef.current = orderedRevisions; 51 55 selectedChangeIdRef.current = selectedChangeId; 52 56 onNavigateRef.current = onNavigate; 53 57 scrollToChangeIdRef.current = scrollToChangeId; 58 + disableBasicNavigationRef.current = disableBasicNavigation; 54 59 55 60 useKeySequence({ 56 61 sequence: "gg", ··· 96 101 event.code === "NumpadAdd"; 97 102 98 103 switch (true) { 99 - case event.key === "j" || event.key === "ArrowDown": 104 + case (event.key === "j" || event.key === "ArrowDown") && !disableBasicNavigationRef.current: 100 105 if (currentIndex >= 0 && currentIndex < revisions.length - 1) { 101 106 targetChangeId = revisions[currentIndex + 1].change_id; 102 107 scrollMode = "step"; ··· 104 109 event.preventDefault(); 105 110 break; 106 111 107 - case event.key === "k" || event.key === "ArrowUp": 112 + case (event.key === "k" || event.key === "ArrowUp") && !disableBasicNavigationRef.current: 108 113 if (currentIndex > 0) { 109 114 targetChangeId = revisions[currentIndex - 1].change_id; 110 115 scrollMode = "step"; ··· 112 117 event.preventDefault(); 113 118 break; 114 119 115 - case event.key === "J" || isMinusKey: 120 + case isMinusKey: 116 121 if (currentRevision) { 117 122 // Navigate to parent revision 118 123 const parentId = ··· 128 133 event.preventDefault(); 129 134 break; 130 135 131 - case event.key === "K" || isPlusKey: 136 + case isPlusKey: 132 137 if (currentRevision) { 133 138 // Find child by checking if any revision has current as parent 134 139 const childRevision = revisions.find(
+92
apps/desktop/src/hooks/useTauriLiveStoreSync.ts
··· 1 + /** 2 + * Tauri → BroadcastChannel Relay 3 + * 4 + * Relays Tauri events to the worker's sync backend via BroadcastChannel. 5 + * The sync backend handles everything else (LiveStore processing, materialization). 6 + * 7 + * Flow: 8 + * 1. Rust backend commits to SQLite + emits livestore:event 9 + * 2. This relay forwards event via BroadcastChannel 10 + * 3. Worker sync backend receives and processes through LiveStore 11 + * 4. Materialized views update → React queries re-render 12 + */ 13 + 14 + import { Channel, invoke } from "@tauri-apps/api/core"; 15 + import { listen, type UnlistenFn } from "@tauri-apps/api/event"; 16 + import { useEffect } from "react"; 17 + import { 18 + TAURI_SYNC_CHANNEL, 19 + TAURI_SYNC_REQUEST, 20 + type PersistedEvent, 21 + } from "../livestore/tauri-adapter"; 22 + 23 + const isTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; 24 + 25 + export function useTauriLiveStoreSync(): void { 26 + useEffect(() => { 27 + if (!isTauri) { 28 + console.log("[TauriRelay] Not in Tauri, skipping"); 29 + return; 30 + } 31 + 32 + let mounted = true; 33 + let unlistenEvent: UnlistenFn | null = null; 34 + 35 + // Channel to relay events to worker 36 + const syncChannel = new BroadcastChannel(TAURI_SYNC_CHANNEL); 37 + 38 + // Handle hydration requests from worker sync backend 39 + const requestChannel = new BroadcastChannel(TAURI_SYNC_REQUEST); 40 + requestChannel.onmessage = async (event) => { 41 + if (!mounted || event.data.type !== "hydrate") return; 42 + 43 + const { afterSequence, storeId } = event.data; 44 + console.log("[TauriRelay] Hydrating from seq:", afterSequence); 45 + 46 + try { 47 + const channel = new Channel<PersistedEvent | { done: true }>(); 48 + 49 + channel.onmessage = (msg) => { 50 + if (!mounted) return; 51 + if ("done" in msg) { 52 + console.log("[TauriRelay] Hydration complete"); 53 + } else { 54 + console.log("[TauriRelay] Hydrating:", msg.name, "seq:", msg.sequence); 55 + syncChannel.postMessage(msg); 56 + } 57 + }; 58 + 59 + await invoke("plugin:livestore|stream_events", { 60 + storeId, 61 + channel, 62 + afterSequence, 63 + }); 64 + } catch (err) { 65 + console.error("[TauriRelay] Hydration failed:", err); 66 + } 67 + }; 68 + 69 + // Relay Tauri events to worker via BroadcastChannel 70 + async function setup() { 71 + try { 72 + unlistenEvent = await listen<PersistedEvent>("livestore:event", (event) => { 73 + if (!mounted) return; 74 + console.log("[TauriRelay] Relaying:", event.payload.name, "seq:", event.payload.sequence); 75 + syncChannel.postMessage(event.payload); 76 + }); 77 + console.log("[TauriRelay] Listening to livestore:event"); 78 + } catch (err) { 79 + console.error("[TauriRelay] Setup failed:", err); 80 + } 81 + } 82 + 83 + setup(); 84 + 85 + return () => { 86 + mounted = false; 87 + unlistenEvent?.(); 88 + requestChannel.close(); 89 + syncChannel.close(); 90 + }; 91 + }, []); 92 + }
+181
apps/desktop/src/livestore/tauri-adapter.ts
··· 1 + import { Channel, invoke } from "@tauri-apps/api/core"; 2 + import { listen, type UnlistenFn } from "@tauri-apps/api/event"; 3 + 4 + // ============================================================================ 5 + // Types 6 + // ============================================================================ 7 + 8 + export interface PersistedEvent { 9 + id: string; 10 + sequence: number; 11 + name: string; 12 + payload: string; 13 + timestamp: number; 14 + clientId: string; 15 + } 16 + 17 + // ============================================================================ 18 + // Channel names for cross-window sync 19 + // ============================================================================ 20 + 21 + export const TAURI_SYNC_CHANNEL = "tauri-livestore-sync"; 22 + export const TAURI_SYNC_REQUEST = "tauri-livestore-request"; 23 + 24 + interface TauriAdapterConfig { 25 + storeId?: string; 26 + batchSize?: number; 27 + flushDebounceMs?: number; 28 + } 29 + 30 + // ============================================================================ 31 + // LiveStore Event Types (simplified interface matching LiveStore's needs) 32 + // ============================================================================ 33 + 34 + interface StoredEvent { 35 + id: string; 36 + sequence: number; 37 + name: string; 38 + payload: unknown; 39 + timestamp: number; 40 + clientId: string; 41 + } 42 + 43 + interface SyncStatus { 44 + pendingEvents: number; 45 + lastPersistedSequence: number; 46 + isOnline: boolean; 47 + } 48 + 49 + // ============================================================================ 50 + // Tauri Sync Adapter 51 + // ============================================================================ 52 + 53 + /** 54 + * TauriSyncAdapter bridges LiveStore's event log to Rust SQLite. 55 + * 56 + * Key design: 57 + * - Events are the unit of sync (not queries, not tables) 58 + * - Writes are batched and debounced for efficiency 59 + * - Hydration streams events via Tauri Channel for backpressure 60 + * - No queries cross the IPC boundary 61 + */ 62 + export function createTauriAdapter(config: TauriAdapterConfig = {}) { 63 + const { storeId = "default", batchSize = 100, flushDebounceMs = 50 } = config; 64 + 65 + let pendingEvents: StoredEvent[] = []; 66 + let flushTimeout: ReturnType<typeof setTimeout> | null = null; 67 + let lastPersistedSequence = 0; 68 + let unlistenFn: UnlistenFn | null = null; 69 + const clientId = crypto.randomUUID(); 70 + 71 + const adapter = { 72 + async init(): Promise<{ lastPersistedSequence: number }> { 73 + await invoke("init_event_store", { storeId }); 74 + lastPersistedSequence = await invoke<number>("get_last_sequence", { storeId }); 75 + return { lastPersistedSequence }; 76 + }, 77 + 78 + async loadEvents(onEvent: (event: StoredEvent) => void, onComplete: () => void): Promise<void> { 79 + const channel = new Channel<PersistedEvent | { done: true }>(); 80 + 81 + channel.onmessage = (message) => { 82 + if ("done" in message) { 83 + onComplete(); 84 + } else { 85 + onEvent({ 86 + id: message.id, 87 + sequence: message.sequence, 88 + name: message.name, 89 + payload: JSON.parse(message.payload), 90 + timestamp: message.timestamp, 91 + clientId: message.clientId, 92 + }); 93 + } 94 + }; 95 + 96 + await invoke("stream_events", { storeId, channel, afterSequence: 0 }); 97 + }, 98 + 99 + onEvent(event: StoredEvent): void { 100 + pendingEvents.push(event); 101 + 102 + if (flushTimeout) clearTimeout(flushTimeout); 103 + 104 + if (pendingEvents.length >= batchSize) { 105 + adapter.flush(); 106 + } else { 107 + flushTimeout = setTimeout(() => adapter.flush(), flushDebounceMs); 108 + } 109 + }, 110 + 111 + async flush(): Promise<void> { 112 + if (pendingEvents.length === 0) return; 113 + 114 + const eventsToFlush = pendingEvents; 115 + pendingEvents = []; 116 + 117 + if (flushTimeout) { 118 + clearTimeout(flushTimeout); 119 + flushTimeout = null; 120 + } 121 + 122 + try { 123 + const serialized = eventsToFlush.map((e) => ({ 124 + id: e.id, 125 + sequence: e.sequence, 126 + name: e.name, 127 + payload: JSON.stringify(e.payload), 128 + timestamp: e.timestamp, 129 + clientId, 130 + })); 131 + 132 + await invoke("persist_events", { storeId, events: serialized }); 133 + lastPersistedSequence = eventsToFlush[eventsToFlush.length - 1].sequence; 134 + } catch (error) { 135 + console.error("Failed to persist events:", error); 136 + // Re-add failed events to front of queue 137 + pendingEvents = [...eventsToFlush, ...pendingEvents]; 138 + throw error; 139 + } 140 + }, 141 + 142 + async subscribe(onExternalEvent: (event: StoredEvent) => void): Promise<void> { 143 + unlistenFn = await listen<PersistedEvent>("livestore:event", (event) => { 144 + // Ignore events from this client 145 + if (event.payload.clientId === clientId) return; 146 + 147 + onExternalEvent({ 148 + id: event.payload.id, 149 + sequence: event.payload.sequence, 150 + name: event.payload.name, 151 + payload: JSON.parse(event.payload.payload), 152 + timestamp: event.payload.timestamp, 153 + clientId: event.payload.clientId, 154 + }); 155 + }); 156 + }, 157 + 158 + getStatus(): SyncStatus { 159 + return { 160 + pendingEvents: pendingEvents.length, 161 + lastPersistedSequence, 162 + isOnline: true, 163 + }; 164 + }, 165 + 166 + async dispose(): Promise<void> { 167 + await adapter.flush(); 168 + if (unlistenFn) { 169 + unlistenFn(); 170 + unlistenFn = null; 171 + } 172 + }, 173 + 174 + clientId, 175 + storeId, 176 + }; 177 + 178 + return adapter; 179 + } 180 + 181 + export type TauriAdapter = ReturnType<typeof createTauriAdapter>;
+154 -13
apps/desktop/src/mocks/setup.ts
··· 18 18 function calculateShortIds(revisionsRaw: Omit<Revision, "change_id_short">[]): Revision[] { 19 19 const changeIds = revisionsRaw.map((r) => r.change_id); 20 20 const result: Revision[] = []; 21 - 21 + 22 22 for (let i = 0; i < changeIds.length; i++) { 23 23 const changeId = changeIds[i]; 24 24 let prefixLen = 1; 25 - 25 + 26 26 // Find minimum prefix length that's unique 27 27 while (prefixLen <= changeId.length) { 28 28 const prefix = changeId.slice(0, prefixLen); 29 29 const matches = changeIds.filter((id) => id.startsWith(prefix)); 30 - 30 + 31 31 // Check if this prefix is unique (only matches this change ID) 32 32 if (matches.length === 1) { 33 33 break; 34 34 } 35 - 35 + 36 36 prefixLen++; 37 37 } 38 - 38 + 39 39 // Handle divergent commits 40 40 const revision = revisionsRaw[i]; 41 41 let changeIdShort: string; ··· 44 44 } else { 45 45 changeIdShort = changeId.slice(0, prefixLen); 46 46 } 47 - 47 + 48 48 result.push({ 49 49 ...revision, 50 50 change_id_short: changeIdShort, 51 51 }); 52 52 } 53 - 53 + 54 54 return result; 55 55 } 56 56 ··· 849 849 ]; 850 850 851 851 // Calculate shortest unique prefixes for all change IDs 852 - const mockRevisions: Revision[] = calculateShortIds(mockRevisionsRaw); 852 + // Made mutable so mutation handlers can update it 853 + let mockRevisions: Revision[] = calculateShortIds(mockRevisionsRaw); 853 854 854 855 const mockChangedFiles: ChangedFile[] = [ 855 856 { path: "src/main.rs", status: "modified" }, ··· 880 881 return mockProjects.find((p) => p.path === path) ?? null; 881 882 }, 882 883 find_repository: () => "/Users/demo/projects/tatami", 883 - get_revisions: () => mockRevisions, 884 + get_revisions: () => { 885 + console.log("[Mock] get_revisions called, returning", mockRevisions.length, "revisions"); 886 + return mockRevisions; 887 + }, 884 888 get_status: (): WorkingCopyStatus => { 885 889 const wc = mockRevisions.find((r) => r.is_working_copy); 886 890 return { ··· 914 918 get_revision_changes: (): ChangedFile[] => mockChangedFiles, 915 919 watch_repository: () => undefined, 916 920 unwatch_repository: () => undefined, 917 - jj_new: () => undefined, 918 - jj_edit: () => undefined, 919 - jj_abandon: () => undefined, 921 + jj_new: (args) => { 922 + const parentChangeIds = args.parentChangeIds as string[]; 923 + // Find parent revisions by change_id (handling short IDs) 924 + const parentRevisions = parentChangeIds 925 + .map((id) => 926 + mockRevisions.find((r) => r.change_id.startsWith(id) || r.change_id_short === id), 927 + ) 928 + .filter((r): r is Revision => r !== undefined); 929 + 930 + // Get parent commit IDs for the new revision 931 + const parentCommitIds = parentRevisions.map((r) => r.commit_id); 932 + 933 + // Find current working copy and clear its flag 934 + const currentWcIndex = mockRevisions.findIndex((r) => r.is_working_copy); 935 + if (currentWcIndex >= 0) { 936 + mockRevisions[currentWcIndex] = { ...mockRevisions[currentWcIndex], is_working_copy: false }; 937 + } 938 + 939 + // Create new revision 940 + const newChangeId = generateChangeId(); 941 + const newCommitId = `new${Date.now().toString(16).slice(-10)}`; 942 + const newRevision: Omit<Revision, "change_id_short"> = { 943 + commit_id: newCommitId, 944 + change_id: newChangeId, 945 + parent_ids: parentCommitIds, 946 + parent_edges: parentCommitIds.map((id) => ({ parent_id: id, edge_type: "direct" as const })), 947 + description: "", 948 + author: "alice@example.com", 949 + timestamp: new Date().toISOString(), 950 + is_working_copy: true, 951 + is_immutable: false, 952 + is_mine: true, 953 + is_trunk: false, 954 + is_divergent: false, 955 + divergent_index: null, 956 + bookmarks: [], 957 + }; 958 + 959 + // Recalculate short IDs with new revision included 960 + const allRevisionsRaw = [ 961 + ...mockRevisions.map(({ change_id_short: _, ...r }) => r), 962 + newRevision, 963 + ]; 964 + mockRevisions = calculateShortIds(allRevisionsRaw); 965 + 966 + return undefined; 967 + }, 968 + jj_edit: (args) => { 969 + const changeId = args.changeId as string; 970 + console.log("[Mock] jj_edit called with changeId:", changeId); 971 + // Find target revision by change_id (handling short IDs) 972 + const targetIndex = mockRevisions.findIndex( 973 + (r) => r.change_id.startsWith(changeId) || r.change_id_short === changeId, 974 + ); 975 + if (targetIndex < 0) { 976 + console.warn(`[Mock] jj_edit: revision not found: ${changeId}`); 977 + return undefined; 978 + } 979 + 980 + console.log("[Mock] jj_edit: found revision at index", targetIndex); 981 + 982 + // Clear working copy from all revisions, set on target 983 + mockRevisions = mockRevisions.map((r, i) => ({ 984 + ...r, 985 + is_working_copy: i === targetIndex, 986 + })); 987 + 988 + console.log( 989 + "[Mock] jj_edit: updated mockRevisions, new WC:", 990 + mockRevisions.find((r) => r.is_working_copy)?.change_id_short, 991 + ); 992 + 993 + return undefined; 994 + }, 995 + jj_abandon: (args) => { 996 + const changeId = args.changeId as string; 997 + // Find revision by change_id (handling short IDs) 998 + const revisionIndex = mockRevisions.findIndex( 999 + (r) => r.change_id.startsWith(changeId) || r.change_id_short === changeId, 1000 + ); 1001 + if (revisionIndex < 0) { 1002 + console.warn(`[Mock] jj_abandon: revision not found: ${changeId}`); 1003 + return undefined; 1004 + } 1005 + 1006 + const revision = mockRevisions[revisionIndex]; 1007 + 1008 + if (revision.is_working_copy) { 1009 + // Abandoning WC creates a new WC on the parent 1010 + // Clear WC flag and create a new working copy 1011 + const parentCommitId = revision.parent_ids[0]; 1012 + // Remove the abandoned revision 1013 + mockRevisions = mockRevisions.filter((_, i) => i !== revisionIndex); 1014 + 1015 + // Create new working copy on parent 1016 + const newChangeId = generateChangeId(); 1017 + const newCommitId = `wc${Date.now().toString(16).slice(-10)}`; 1018 + const newRevision: Omit<Revision, "change_id_short"> = { 1019 + commit_id: newCommitId, 1020 + change_id: newChangeId, 1021 + parent_ids: parentCommitId ? [parentCommitId] : [], 1022 + parent_edges: parentCommitId 1023 + ? [{ parent_id: parentCommitId, edge_type: "direct" as const }] 1024 + : [], 1025 + description: "", 1026 + author: "alice@example.com", 1027 + timestamp: new Date().toISOString(), 1028 + is_working_copy: true, 1029 + is_immutable: false, 1030 + is_mine: true, 1031 + is_trunk: false, 1032 + is_divergent: false, 1033 + divergent_index: null, 1034 + bookmarks: [], 1035 + }; 1036 + 1037 + // Recalculate short IDs 1038 + const allRevisionsRaw = [ 1039 + ...mockRevisions.map(({ change_id_short: _, ...r }) => r), 1040 + newRevision, 1041 + ]; 1042 + mockRevisions = calculateShortIds(allRevisionsRaw); 1043 + } else { 1044 + // Just remove the revision 1045 + mockRevisions = mockRevisions.filter((_, i) => i !== revisionIndex); 1046 + // Recalculate short IDs after removal 1047 + const allRevisionsRaw = mockRevisions.map(({ change_id_short: _, ...r }) => r); 1048 + mockRevisions = calculateShortIds(allRevisionsRaw); 1049 + } 1050 + 1051 + return undefined; 1052 + }, 920 1053 get_commit_recency: () => ({}), 921 1054 resolve_revset: (args) => { 922 1055 const revset = args.revset as string; ··· 958 1091 const { mockIPC } = await import("@tauri-apps/api/mocks"); 959 1092 960 1093 mockIPC((cmd, args) => { 1094 + console.log(`[Mock] IPC call: ${cmd}`, args); 961 1095 const handler = handlers[cmd]; 962 1096 if (!handler) { 963 1097 console.warn(`[Mock] No handler for command: ${cmd}`, args); 964 1098 return undefined; 965 1099 } 966 - return handler((args ?? {}) as Record<string, unknown>); 1100 + try { 1101 + const result = handler((args ?? {}) as Record<string, unknown>); 1102 + console.log(`[Mock] IPC result for ${cmd}:`, result); 1103 + return result; 1104 + } catch (error) { 1105 + console.error(`[Mock] IPC error for ${cmd}:`, error); 1106 + throw error; 1107 + } 967 1108 }); 968 1109 969 1110 console.log("[Mocks] IPC mocks ready");
+7
apps/desktop/src/routes/project.$projectId.tsx
··· 9 9 rev?: string; 10 10 file?: string; 11 11 expanded?: boolean; 12 + stack?: string; // Focused collapsed stack id 13 + selected?: string; // Comma-separated list of selected revision changeIds 14 + selectionAnchor?: string; // changeId where shift-selection started 12 15 }; 13 16 14 17 export const Route = createRoute({ ··· 19 22 rev: typeof search.rev === "string" ? search.rev : undefined, 20 23 file: typeof search.file === "string" ? search.file : undefined, 21 24 expanded: search.expanded === true || search.expanded === "true", 25 + stack: typeof search.stack === "string" ? search.stack : undefined, 26 + selected: typeof search.selected === "string" ? search.selected : undefined, 27 + selectionAnchor: 28 + typeof search.selectionAnchor === "string" ? search.selectionAnchor : undefined, 22 29 }; 23 30 }, 24 31 component: ProjectComponent,
+3 -4
apps/desktop/src/routes/repositories.tsx
··· 13 13 } from "@/components/ui/alert-dialog"; 14 14 import { Button } from "@/components/ui/button"; 15 15 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 16 - import { repositoriesCollection } from "@/db"; 16 + import { deleteRepository, repositoriesCollection } from "@/db"; 17 17 import { useKeyboardShortcut } from "@/hooks/useKeyboard"; 18 - import { type Repository, removeRepository } from "@/tauri-commands"; 18 + import type { Repository } from "@/tauri-commands"; 19 19 import { Route as rootRoute } from "./__root"; 20 20 21 21 export const Route = createRoute({ ··· 39 39 if (!pendingDelete || isDeleting) return; 40 40 setIsDeleting(true); 41 41 try { 42 - await removeRepository(pendingDelete.id); 43 - repositoriesCollection.utils.writeDelete(pendingDelete.id); 42 + await deleteRepository(repositoriesCollection, pendingDelete.id); 44 43 } finally { 45 44 setIsDeleting(false); 46 45 setPendingDelete(null);
+1 -3
apps/desktop/src/routes/settings.tsx
··· 34 34 {/* Content */} 35 35 <div className="flex-1 overflow-auto p-6"> 36 36 <div className="max-w-2xl space-y-8"> 37 - <div className="text-muted-foreground text-sm"> 38 - Settings coming soon... 39 - </div> 37 + <div className="text-muted-foreground text-sm">Settings coming soon...</div> 40 38 </div> 41 39 </div> 42 40 </div>
+1 -4
apps/desktop/src/tauri-commands.ts
··· 96 96 } 97 97 98 98 /** Resolve a revset expression using jj-lib's full parser */ 99 - export async function resolveRevset( 100 - repoPath: string, 101 - revset: string, 102 - ): Promise<RevsetResult> { 99 + export async function resolveRevset(repoPath: string, revset: string): Promise<RevsetResult> { 103 100 return invoke<RevsetResult>("resolve_revset", { repoPath, revset }); 104 101 }
+233 -7
bun.lock
··· 50 50 }, 51 51 "devDependencies": { 52 52 "@biomejs/biome": "^2.3.10", 53 + "@tatami/vite-plugin-annotator": "workspace:*", 53 54 "@tauri-apps/cli": "^2.1.0", 54 55 "@types/node": "^25.0.3", 55 56 "@types/react": "19", ··· 60 61 "tailwindcss": "^4.1.18", 61 62 "typescript": "^5.6.3", 62 63 "vite": "^5.4.11", 64 + }, 65 + }, 66 + "packages/vite-plugin-annotator": { 67 + "name": "@tatami/vite-plugin-annotator", 68 + "version": "0.1.0", 69 + "dependencies": { 70 + "bippy": "^0.2.11", 71 + "solid-js": "^1.9.5", 72 + }, 73 + "devDependencies": { 74 + "@types/node": "^25.0.9", 75 + "esbuild-plugin-solid": "^0.6.0", 76 + "tsup": "^8.4.0", 77 + "typescript": "^5.8.3", 78 + "vite": "^6.3.5", 79 + "vite-plugin-solid": "^2.11.6", 80 + }, 81 + "peerDependencies": { 82 + "vite": "^5.0.0 || ^6.0.0", 63 83 }, 64 84 }, 65 85 }, ··· 198 218 199 219 "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], 200 220 221 + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], 222 + 201 223 "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], 202 224 225 + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], 226 + 203 227 "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], 228 + 229 + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], 204 230 205 231 "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], 206 232 ··· 442 468 443 469 "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.16", "", {}, "sha512-njazUC8mDkrxWmyZmn/3eXrDcP8Msb3chSr4q6a65RmwdSbMlMCdnOphv6/8mLO7O3Fuza5s4M4DclmvAO5w0w=="], 444 470 471 + "@tatami/vite-plugin-annotator": ["@tatami/vite-plugin-annotator@workspace:packages/vite-plugin-annotator"], 472 + 445 473 "@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="], 446 474 447 475 "@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="], ··· 494 522 495 523 "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], 496 524 497 - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], 525 + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], 498 526 499 527 "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], 500 528 501 529 "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 502 530 531 + "@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="], 532 + 503 533 "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], 504 534 505 535 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], ··· 526 556 527 557 "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], 528 558 559 + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], 560 + 529 561 "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], 530 562 531 563 "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], ··· 538 570 539 571 "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], 540 572 573 + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], 574 + 541 575 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 542 576 543 577 "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], 544 578 545 579 "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], 580 + 581 + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.3", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w=="], 546 582 547 583 "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], 548 584 585 + "babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], 586 + 549 587 "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], 588 + 589 + "bippy": ["bippy@0.2.24", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": ">=17.0.1" } }, "sha512-EZ8GSYSyPywsUmcOH2Kss/yhI8Auoku1WGKOK3/Ya7vukriRPJ2/8q+KApvh8LtX4KXNDBE5QD6furYz2Yei+Q=="], 550 590 551 591 "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], 552 592 ··· 556 596 557 597 "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], 558 598 599 + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], 600 + 559 601 "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 560 602 603 + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], 604 + 561 605 "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 562 606 563 607 "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], ··· 573 617 "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], 574 618 575 619 "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], 620 + 621 + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], 576 622 577 623 "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], 578 624 ··· 597 643 "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], 598 644 599 645 "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], 646 + 647 + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], 648 + 649 + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], 600 650 601 651 "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], 602 652 ··· 666 716 667 717 "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], 668 718 719 + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], 720 + 669 721 "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], 670 722 671 723 "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], ··· 677 729 "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 678 730 679 731 "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], 732 + 733 + "esbuild-plugin-solid": ["esbuild-plugin-solid@0.6.0", "", { "dependencies": { "@babel/core": "^7.20.12", "@babel/preset-typescript": "^7.18.6", "babel-preset-solid": "^1.6.9" }, "peerDependencies": { "esbuild": ">=0.20", "solid-js": ">= 1.0" } }, "sha512-V1FvDALwLDX6K0XNYM9CMRAnMzA0+Ecu55qBUT9q/eAJh1KIDsTMFoOzMSgyHqbOfvrVfO3Mws3z7TW2GVnIZA=="], 680 734 681 735 "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 682 736 ··· 718 772 719 773 "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], 720 774 775 + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], 776 + 721 777 "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], 722 778 723 779 "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], ··· 771 827 "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], 772 828 773 829 "hono": ["hono@4.11.1", "", {}, "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg=="], 830 + 831 + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], 774 832 775 833 "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], 776 834 ··· 822 880 823 881 "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], 824 882 883 + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], 884 + 825 885 "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 826 886 827 887 "isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="], ··· 831 891 "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], 832 892 833 893 "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], 894 + 895 + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], 834 896 835 897 "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 836 898 ··· 874 936 875 937 "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], 876 938 939 + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], 940 + 877 941 "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], 942 + 943 + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], 878 944 879 945 "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], 880 946 ··· 892 958 893 959 "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], 894 960 961 + "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], 962 + 895 963 "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], 896 964 897 965 "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], ··· 921 989 "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], 922 990 923 991 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 992 + 993 + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], 924 994 925 995 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 926 996 ··· 934 1004 935 1005 "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], 936 1006 1007 + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], 1008 + 937 1009 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 938 1010 939 1011 "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], ··· 978 1050 979 1051 "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], 980 1052 1053 + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], 1054 + 981 1055 "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 982 1056 983 1057 "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], ··· 986 1060 987 1061 "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], 988 1062 1063 + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 1064 + 989 1065 "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 990 1066 991 1067 "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 992 1068 1069 + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], 1070 + 993 1071 "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], 994 1072 1073 + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], 1074 + 995 1075 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], 996 1076 1077 + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], 1078 + 997 1079 "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], 998 1080 999 1081 "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], ··· 1030 1112 1031 1113 "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], 1032 1114 1115 + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], 1116 + 1033 1117 "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], 1034 1118 1035 1119 "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], ··· 1044 1128 1045 1129 "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], 1046 1130 1047 - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 1131 + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], 1048 1132 1049 1133 "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], 1050 1134 ··· 1068 1152 1069 1153 "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], 1070 1154 1071 - "seroval": ["seroval@1.4.0", "", {}, "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg=="], 1155 + "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], 1072 1156 1073 - "seroval-plugins": ["seroval-plugins@1.4.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ=="], 1157 + "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], 1074 1158 1075 1159 "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], 1076 1160 ··· 1096 1180 1097 1181 "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], 1098 1182 1183 + "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], 1184 + 1185 + "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], 1186 + 1099 1187 "sorted-btree": ["sorted-btree@1.8.1", "", {}, "sha512-395+XIP+wqNn3USkFSrNz7G3Ss/MXlZEqesxvzCRFwL14h6e8LukDHdLBePn5pwbm5OQ9vGu8mDyz2lLDIqamQ=="], 1100 1188 1101 - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], 1189 + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], 1102 1190 1103 1191 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 1104 1192 ··· 1122 1210 1123 1211 "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], 1124 1212 1213 + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], 1214 + 1125 1215 "tabbable": ["tabbable@6.3.0", "", {}, "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ=="], 1126 1216 1127 1217 "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], ··· 1132 1222 1133 1223 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], 1134 1224 1225 + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], 1226 + 1227 + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], 1228 + 1135 1229 "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], 1136 1230 1137 1231 "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], 1138 1232 1139 - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], 1233 + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], 1234 + 1235 + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 1140 1236 1141 1237 "tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="], 1142 1238 ··· 1148 1244 1149 1245 "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], 1150 1246 1247 + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], 1248 + 1151 1249 "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], 1250 + 1251 + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], 1152 1252 1153 1253 "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], 1154 1254 ··· 1156 1256 1157 1257 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1158 1258 1259 + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], 1260 + 1159 1261 "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], 1160 1262 1161 1263 "type-fest": ["type-fest@5.3.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg=="], ··· 1163 1265 "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], 1164 1266 1165 1267 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 1268 + 1269 + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], 1166 1270 1167 1271 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 1168 1272 ··· 1202 1306 1203 1307 "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], 1204 1308 1205 - "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], 1309 + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], 1310 + 1311 + "vite-plugin-solid": ["vite-plugin-solid@2.11.10", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw=="], 1312 + 1313 + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], 1206 1314 1207 1315 "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], 1208 1316 ··· 1231 1339 "zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="], 1232 1340 1233 1341 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], 1342 + 1343 + "@antfu/ni/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], 1234 1344 1235 1345 "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], 1236 1346 ··· 1258 1368 1259 1369 "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1260 1370 1371 + "@tanstack/router-core/seroval": ["seroval@1.4.0", "", {}, "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg=="], 1372 + 1373 + "@tanstack/router-core/seroval-plugins": ["seroval-plugins@1.4.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ=="], 1374 + 1375 + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], 1376 + 1261 1377 "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1262 1378 1263 1379 "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], ··· 1265 1381 "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 1266 1382 1267 1383 "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1384 + 1385 + "desktop/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], 1268 1386 1269 1387 "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 1270 1388 1389 + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 1390 + 1271 1391 "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], 1272 1392 1273 1393 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], ··· 1276 1396 1277 1397 "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], 1278 1398 1399 + "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], 1400 + 1279 1401 "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], 1280 1402 1281 1403 "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], 1404 + 1405 + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], 1406 + 1407 + "tsup/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], 1408 + 1409 + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 1282 1410 1283 1411 "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1284 1412 ··· 1303 1431 "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 1304 1432 1305 1433 "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 1434 + 1435 + "tsup/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], 1436 + 1437 + "tsup/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], 1438 + 1439 + "tsup/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], 1440 + 1441 + "tsup/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], 1442 + 1443 + "tsup/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], 1444 + 1445 + "tsup/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], 1446 + 1447 + "tsup/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], 1448 + 1449 + "tsup/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], 1450 + 1451 + "tsup/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], 1452 + 1453 + "tsup/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], 1454 + 1455 + "tsup/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], 1456 + 1457 + "tsup/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], 1458 + 1459 + "tsup/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], 1460 + 1461 + "tsup/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], 1462 + 1463 + "tsup/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], 1464 + 1465 + "tsup/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], 1466 + 1467 + "tsup/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], 1468 + 1469 + "tsup/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], 1470 + 1471 + "tsup/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], 1472 + 1473 + "tsup/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], 1474 + 1475 + "tsup/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], 1476 + 1477 + "tsup/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], 1478 + 1479 + "tsup/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], 1480 + 1481 + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 1482 + 1483 + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 1484 + 1485 + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 1486 + 1487 + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 1488 + 1489 + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 1490 + 1491 + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 1492 + 1493 + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 1494 + 1495 + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 1496 + 1497 + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 1498 + 1499 + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 1500 + 1501 + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 1502 + 1503 + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 1504 + 1505 + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 1506 + 1507 + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 1508 + 1509 + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 1510 + 1511 + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 1512 + 1513 + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 1514 + 1515 + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 1516 + 1517 + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 1518 + 1519 + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 1520 + 1521 + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 1522 + 1523 + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 1524 + 1525 + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 1526 + 1527 + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 1528 + 1529 + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 1530 + 1531 + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 1306 1532 1307 1533 "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 1308 1534
+2 -1
package.json
··· 3 3 "version": "0.1.0", 4 4 "private": true, 5 5 "workspaces": [ 6 - "apps/*" 6 + "apps/*", 7 + "packages/*" 7 8 ], 8 9 "engines": { 9 10 "node": ">=18"
+423
packages/vite-plugin-annotator/bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "@tatami/vite-plugin-annotator", 7 + "dependencies": { 8 + "bippy": "^0.2.11", 9 + "solid-js": "^1.9.5", 10 + }, 11 + "devDependencies": { 12 + "@types/node": "^25.0.9", 13 + "esbuild-plugin-solid": "^0.6.0", 14 + "tsup": "^8.4.0", 15 + "typescript": "^5.8.3", 16 + "vite": "^6.3.5", 17 + "vite-plugin-solid": "^2.11.6", 18 + }, 19 + "peerDependencies": { 20 + "vite": "^5.0.0 || ^6.0.0", 21 + }, 22 + }, 23 + }, 24 + "packages": { 25 + "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], 26 + 27 + "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], 28 + 29 + "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], 30 + 31 + "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], 32 + 33 + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], 34 + 35 + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], 36 + 37 + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], 38 + 39 + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], 40 + 41 + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], 42 + 43 + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], 44 + 45 + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], 46 + 47 + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], 48 + 49 + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], 50 + 51 + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], 52 + 53 + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], 54 + 55 + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], 56 + 57 + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], 58 + 59 + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], 60 + 61 + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], 62 + 63 + "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], 64 + 65 + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], 66 + 67 + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], 68 + 69 + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], 70 + 71 + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], 72 + 73 + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], 74 + 75 + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], 76 + 77 + "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], 78 + 79 + "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], 80 + 81 + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], 82 + 83 + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], 84 + 85 + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], 86 + 87 + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], 88 + 89 + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], 90 + 91 + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], 92 + 93 + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], 94 + 95 + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], 96 + 97 + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], 98 + 99 + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], 100 + 101 + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], 102 + 103 + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], 104 + 105 + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], 106 + 107 + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], 108 + 109 + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], 110 + 111 + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], 112 + 113 + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], 114 + 115 + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], 116 + 117 + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], 118 + 119 + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], 120 + 121 + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], 122 + 123 + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], 124 + 125 + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], 126 + 127 + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], 128 + 129 + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], 130 + 131 + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], 132 + 133 + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], 134 + 135 + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], 136 + 137 + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 138 + 139 + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 140 + 141 + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], 142 + 143 + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], 144 + 145 + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], 146 + 147 + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], 148 + 149 + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], 150 + 151 + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], 152 + 153 + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], 154 + 155 + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], 156 + 157 + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], 158 + 159 + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], 160 + 161 + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], 162 + 163 + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], 164 + 165 + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], 166 + 167 + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], 168 + 169 + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], 170 + 171 + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], 172 + 173 + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], 174 + 175 + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], 176 + 177 + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], 178 + 179 + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], 180 + 181 + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], 182 + 183 + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], 184 + 185 + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], 186 + 187 + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], 188 + 189 + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], 190 + 191 + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], 192 + 193 + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], 194 + 195 + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], 196 + 197 + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], 198 + 199 + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], 200 + 201 + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 202 + 203 + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], 204 + 205 + "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], 206 + 207 + "@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="], 208 + 209 + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], 210 + 211 + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], 212 + 213 + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.3", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w=="], 214 + 215 + "babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], 216 + 217 + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg=="], 218 + 219 + "bippy": ["bippy@0.2.24", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": ">=17.0.1" } }, "sha512-EZ8GSYSyPywsUmcOH2Kss/yhI8Auoku1WGKOK3/Ya7vukriRPJ2/8q+KApvh8LtX4KXNDBE5QD6furYz2Yei+Q=="], 220 + 221 + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], 222 + 223 + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], 224 + 225 + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], 226 + 227 + "caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="], 228 + 229 + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], 230 + 231 + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], 232 + 233 + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], 234 + 235 + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], 236 + 237 + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 238 + 239 + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 240 + 241 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 242 + 243 + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], 244 + 245 + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], 246 + 247 + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], 248 + 249 + "esbuild-plugin-solid": ["esbuild-plugin-solid@0.6.0", "", { "dependencies": { "@babel/core": "^7.20.12", "@babel/preset-typescript": "^7.18.6", "babel-preset-solid": "^1.6.9" }, "peerDependencies": { "esbuild": ">=0.20", "solid-js": ">= 1.0" } }, "sha512-V1FvDALwLDX6K0XNYM9CMRAnMzA0+Ecu55qBUT9q/eAJh1KIDsTMFoOzMSgyHqbOfvrVfO3Mws3z7TW2GVnIZA=="], 250 + 251 + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 252 + 253 + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 254 + 255 + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], 256 + 257 + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 258 + 259 + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], 260 + 261 + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], 262 + 263 + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], 264 + 265 + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], 266 + 267 + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 268 + 269 + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 270 + 271 + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], 272 + 273 + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], 274 + 275 + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], 276 + 277 + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], 278 + 279 + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], 280 + 281 + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 282 + 283 + "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], 284 + 285 + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], 286 + 287 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 288 + 289 + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], 290 + 291 + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 292 + 293 + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], 294 + 295 + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], 296 + 297 + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], 298 + 299 + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 300 + 301 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 302 + 303 + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 304 + 305 + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], 306 + 307 + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], 308 + 309 + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], 310 + 311 + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], 312 + 313 + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], 314 + 315 + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], 316 + 317 + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], 318 + 319 + "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], 320 + 321 + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 322 + 323 + "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], 324 + 325 + "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], 326 + 327 + "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], 328 + 329 + "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], 330 + 331 + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], 332 + 333 + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 334 + 335 + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], 336 + 337 + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], 338 + 339 + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], 340 + 341 + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], 342 + 343 + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 344 + 345 + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], 346 + 347 + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], 348 + 349 + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], 350 + 351 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 352 + 353 + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], 354 + 355 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 356 + 357 + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], 358 + 359 + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], 360 + 361 + "vite-plugin-solid": ["vite-plugin-solid@2.11.10", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw=="], 362 + 363 + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], 364 + 365 + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], 366 + 367 + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], 368 + 369 + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 370 + 371 + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 372 + 373 + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 374 + 375 + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 376 + 377 + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 378 + 379 + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 380 + 381 + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 382 + 383 + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 384 + 385 + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 386 + 387 + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 388 + 389 + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 390 + 391 + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 392 + 393 + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 394 + 395 + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 396 + 397 + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 398 + 399 + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 400 + 401 + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 402 + 403 + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 404 + 405 + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 406 + 407 + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 408 + 409 + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 410 + 411 + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 412 + 413 + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 414 + 415 + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 416 + 417 + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 418 + 419 + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 420 + 421 + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 422 + } 423 + }
+27
packages/vite-plugin-annotator/package.json
··· 1 + { 2 + "name": "@tatami/vite-plugin-annotator", 3 + "version": "0.1.0", 4 + "type": "module", 5 + "main": "dist/index.js", 6 + "types": "dist/index.d.ts", 7 + "files": ["dist"], 8 + "scripts": { 9 + "build": "tsup", 10 + "dev": "tsup --watch" 11 + }, 12 + "peerDependencies": { 13 + "vite": "^5.0.0 || ^6.0.0" 14 + }, 15 + "dependencies": { 16 + "bippy": "^0.2.11", 17 + "solid-js": "^1.9.5" 18 + }, 19 + "devDependencies": { 20 + "@types/node": "^25.0.9", 21 + "esbuild-plugin-solid": "^0.6.0", 22 + "tsup": "^8.4.0", 23 + "typescript": "^5.8.3", 24 + "vite": "^6.3.5", 25 + "vite-plugin-solid": "^2.11.6" 26 + } 27 + }
+99
packages/vite-plugin-annotator/src/client/animations.ts
··· 1 + import type { AnimationSnapshot } from "./types"; 2 + 3 + /** 4 + * Get a CSS selector path for an element 5 + */ 6 + export function getSelector(el: Element): string { 7 + if (el.id) return `#${el.id}`; 8 + 9 + const parts: string[] = []; 10 + let current: Element | null = el; 11 + 12 + while (current && current !== document.body) { 13 + let selector = current.tagName.toLowerCase(); 14 + 15 + if (current.id) { 16 + selector = `#${current.id}`; 17 + parts.unshift(selector); 18 + break; 19 + } 20 + 21 + if (current.className && typeof current.className === "string") { 22 + const classes = current.className.trim().split(/\s+/).slice(0, 2); 23 + if (classes.length > 0 && classes[0]) { 24 + selector += `.${classes.join(".")}`; 25 + } 26 + } 27 + 28 + const parent = current.parentElement; 29 + if (parent) { 30 + const siblings = Array.from(parent.children).filter( 31 + (c) => c.tagName === current!.tagName 32 + ); 33 + if (siblings.length > 1) { 34 + const index = siblings.indexOf(current) + 1; 35 + selector += `:nth-of-type(${index})`; 36 + } 37 + } 38 + 39 + parts.unshift(selector); 40 + current = parent; 41 + } 42 + 43 + return parts.join(" > "); 44 + } 45 + 46 + /** 47 + * Get animation snapshots for a specific element (captures current state) 48 + */ 49 + export function getElementAnimations(element: Element): AnimationSnapshot[] { 50 + const animations = element.getAnimations({ subtree: false }); 51 + const snapshots: AnimationSnapshot[] = []; 52 + 53 + for (const anim of animations) { 54 + const effect = anim.effect; 55 + const target = effect?.target as Element | null; 56 + if (!target) continue; 57 + 58 + const timing = effect?.getComputedTiming(); 59 + const currentTime = anim.currentTime ?? 0; 60 + const duration = 61 + typeof timing?.duration === "number" ? timing.duration : 0; 62 + const progress = timing?.progress ?? 0; 63 + 64 + let name = "animation"; 65 + let easing: string | undefined; 66 + let properties: string[] | undefined; 67 + 68 + if (anim instanceof CSSAnimation) { 69 + name = anim.animationName; 70 + } else if (anim instanceof CSSTransition) { 71 + name = "transition"; 72 + properties = [anim.transitionProperty]; 73 + } 74 + 75 + if (effect instanceof KeyframeEffect) { 76 + const keyframeTiming = effect.getTiming(); 77 + easing = 78 + typeof keyframeTiming.easing === "string" 79 + ? keyframeTiming.easing 80 + : undefined; 81 + } 82 + 83 + snapshots.push({ 84 + targetId: getSelector(target), 85 + selector: getSelector(target), 86 + name, 87 + currentTime: Number(currentTime), 88 + duration, 89 + progress, 90 + playState: anim.playState, 91 + properties, 92 + easing, 93 + iterations: timing?.iterations ?? 1, 94 + currentIteration: timing?.currentIteration ?? 0, 95 + }); 96 + } 97 + 98 + return snapshots; 99 + }
+317
packages/vite-plugin-annotator/src/client/components/Overlay.tsx
··· 1 + import { Show, For, createSignal, onMount, onCleanup, createEffect } from "solid-js"; 2 + import { 3 + state, 4 + setHoveredElement, 5 + addAnnotation, 6 + selectAnnotation, 7 + } from "../store"; 8 + import type { Annotation } from "../types"; 9 + 10 + /** 11 + * Injects cursor style into main document (not shadow DOM) 12 + */ 13 + function CursorStyle() { 14 + let styleEl: HTMLStyleElement | null = null; 15 + 16 + createEffect(() => { 17 + const isActive = state().isActive; 18 + 19 + if (isActive && !styleEl) { 20 + styleEl = document.createElement("style"); 21 + styleEl.id = "annotator-cursor-style"; 22 + styleEl.textContent = ` 23 + body { cursor: crosshair !important; } 24 + body * { cursor: crosshair !important; } 25 + [data-annotator] { cursor: default !important; } 26 + [data-annotator] * { cursor: pointer !important; } 27 + [data-annotator] input { cursor: text !important; } 28 + `; 29 + document.head.appendChild(styleEl); 30 + } else if (!isActive && styleEl) { 31 + styleEl.remove(); 32 + styleEl = null; 33 + } 34 + }); 35 + 36 + onCleanup(() => { 37 + styleEl?.remove(); 38 + }); 39 + 40 + return null; 41 + } 42 + 43 + /** 44 + * Highlight overlay shown when hovering elements in active mode 45 + */ 46 + function HoverHighlight() { 47 + const element = () => state().hoveredElement; 48 + 49 + const rect = () => { 50 + const el = element(); 51 + if (!el) return null; 52 + return el.getBoundingClientRect(); 53 + }; 54 + 55 + return ( 56 + <Show when={rect()}> 57 + {(r) => ( 58 + <div 59 + data-annotator 60 + style={{ 61 + position: "fixed", 62 + top: `${r().top}px`, 63 + left: `${r().left}px`, 64 + width: `${r().width}px`, 65 + height: `${r().height}px`, 66 + border: "2px solid #3b82f6", 67 + "background-color": "rgba(59, 130, 246, 0.1)", 68 + "pointer-events": "none", 69 + "border-radius": "4px", 70 + "z-index": "2147483644", 71 + transition: "all 0.05s ease-out", 72 + }} 73 + /> 74 + )} 75 + </Show> 76 + ); 77 + } 78 + 79 + /** 80 + * Marker for an existing annotation 81 + */ 82 + function AnnotationMarker(props: { annotation: Annotation; index: number }) { 83 + const isSelected = () => state().selectedId === props.annotation.id; 84 + 85 + return ( 86 + <div 87 + data-annotator 88 + style={{ 89 + position: "fixed", 90 + top: `${props.annotation.position.y - 10}px`, 91 + left: `${props.annotation.position.x - 10}px`, 92 + width: "20px", 93 + height: "20px", 94 + "border-radius": "50%", 95 + "background-color": isSelected() ? "#3b82f6" : "#f97316", 96 + border: "2px solid white", 97 + "box-shadow": "0 1px 4px rgba(0,0,0,0.3)", 98 + cursor: "pointer", 99 + "z-index": "2147483645", 100 + display: "flex", 101 + "align-items": "center", 102 + "justify-content": "center", 103 + "font-size": "10px", 104 + "font-weight": "bold", 105 + color: "white", 106 + transition: "transform 0.1s ease-out", 107 + transform: isSelected() ? "scale(1.2)" : "scale(1)", 108 + }} 109 + onClick={(e) => { 110 + e.stopPropagation(); 111 + selectAnnotation(isSelected() ? null : props.annotation.id); 112 + }} 113 + title={props.annotation.text} 114 + > 115 + {props.index + 1} 116 + </div> 117 + ); 118 + } 119 + 120 + /** 121 + * Input popover for adding annotation text 122 + */ 123 + function AnnotationInput(props: { 124 + position: { x: number; y: number }; 125 + element: Element; 126 + onSubmit: (text: string) => void; 127 + onCancel: () => void; 128 + }) { 129 + const [text, setText] = createSignal(""); 130 + let inputRef: HTMLInputElement | undefined; 131 + 132 + onMount(() => { 133 + requestAnimationFrame(() => { 134 + inputRef?.focus(); 135 + }); 136 + }); 137 + 138 + const handleSubmit = () => { 139 + const value = text().trim(); 140 + if (value) { 141 + props.onSubmit(value); 142 + } else { 143 + props.onCancel(); 144 + } 145 + }; 146 + 147 + const handleKeyDown = (e: KeyboardEvent) => { 148 + if (e.key === "Enter" && !e.shiftKey) { 149 + e.preventDefault(); 150 + handleSubmit(); 151 + } else if (e.key === "Escape") { 152 + props.onCancel(); 153 + } 154 + }; 155 + 156 + const popoverStyle = () => { 157 + let x = props.position.x + 12; 158 + let y = props.position.y + 12; 159 + 160 + const popoverWidth = 240; 161 + const popoverHeight = 72; 162 + 163 + if (x + popoverWidth > window.innerWidth - 16) { 164 + x = props.position.x - popoverWidth - 12; 165 + } 166 + if (y + popoverHeight > window.innerHeight - 16) { 167 + y = props.position.y - popoverHeight - 12; 168 + } 169 + 170 + return { 171 + position: "fixed" as const, 172 + top: `${y}px`, 173 + left: `${x}px`, 174 + width: `${popoverWidth}px`, 175 + "z-index": "2147483646", 176 + }; 177 + }; 178 + 179 + return ( 180 + <div data-annotator style={popoverStyle()} onClick={(e) => e.stopPropagation()}> 181 + <div 182 + style={{ 183 + "background-color": "white", 184 + "border-radius": "8px", 185 + "box-shadow": "0 4px 16px rgba(0,0,0,0.2)", 186 + padding: "10px", 187 + display: "flex", 188 + gap: "8px", 189 + }} 190 + > 191 + <input 192 + ref={inputRef} 193 + type="text" 194 + placeholder="What's wrong?" 195 + value={text()} 196 + onInput={(e) => setText(e.currentTarget.value)} 197 + onKeyDown={handleKeyDown} 198 + style={{ 199 + flex: "1", 200 + padding: "8px 10px", 201 + border: "1px solid #e5e7eb", 202 + "border-radius": "6px", 203 + "font-size": "13px", 204 + outline: "none", 205 + "min-width": "0", 206 + }} 207 + onFocus={(e) => { 208 + e.currentTarget.style.borderColor = "#3b82f6"; 209 + }} 210 + onBlur={(e) => { 211 + e.currentTarget.style.borderColor = "#e5e7eb"; 212 + }} 213 + /> 214 + <button 215 + onClick={handleSubmit} 216 + style={{ 217 + padding: "8px 12px", 218 + border: "none", 219 + "border-radius": "6px", 220 + background: "#3b82f6", 221 + color: "white", 222 + cursor: "pointer", 223 + "font-size": "13px", 224 + "white-space": "nowrap", 225 + }} 226 + > 227 + Add 228 + </button> 229 + </div> 230 + </div> 231 + ); 232 + } 233 + 234 + /** 235 + * Main overlay component 236 + */ 237 + export function Overlay() { 238 + const [pendingAnnotation, setPendingAnnotation] = createSignal<{ 239 + position: { x: number; y: number }; 240 + element: Element; 241 + } | null>(null); 242 + 243 + const handleMouseMove = (e: MouseEvent) => { 244 + if (!state().isActive || pendingAnnotation()) { 245 + if (!state().isActive) setHoveredElement(null); 246 + return; 247 + } 248 + 249 + const elements = document.elementsFromPoint(e.clientX, e.clientY); 250 + const target = elements.find((el) => !el.closest("[data-annotator]")); 251 + setHoveredElement(target ?? null); 252 + }; 253 + 254 + const handleClick = (e: MouseEvent) => { 255 + if (!state().isActive) return; 256 + 257 + const target = e.target as Element; 258 + if (target.closest("[data-annotator]")) { 259 + return; 260 + } 261 + 262 + e.preventDefault(); 263 + e.stopPropagation(); 264 + 265 + const element = state().hoveredElement; 266 + if (!element) return; 267 + 268 + setPendingAnnotation({ 269 + position: { x: e.clientX, y: e.clientY }, 270 + element, 271 + }); 272 + 273 + setHoveredElement(null); 274 + }; 275 + 276 + onMount(() => { 277 + document.addEventListener("mousemove", handleMouseMove, { capture: true }); 278 + document.addEventListener("click", handleClick, { capture: true }); 279 + }); 280 + 281 + onCleanup(() => { 282 + document.removeEventListener("mousemove", handleMouseMove, { capture: true }); 283 + document.removeEventListener("click", handleClick, { capture: true }); 284 + }); 285 + 286 + const handleAnnotationSubmit = async (text: string) => { 287 + const pending = pendingAnnotation(); 288 + if (!pending) return; 289 + 290 + await addAnnotation(pending.element, pending.position, text); 291 + setPendingAnnotation(null); 292 + }; 293 + 294 + return ( 295 + <> 296 + <CursorStyle /> 297 + <HoverHighlight /> 298 + 299 + <For each={state().annotations}> 300 + {(annotation, index) => ( 301 + <AnnotationMarker annotation={annotation} index={index()} /> 302 + )} 303 + </For> 304 + 305 + <Show when={pendingAnnotation()}> 306 + {(pending) => ( 307 + <AnnotationInput 308 + position={pending().position} 309 + element={pending().element} 310 + onSubmit={handleAnnotationSubmit} 311 + onCancel={() => setPendingAnnotation(null)} 312 + /> 313 + )} 314 + </Show> 315 + </> 316 + ); 317 + }
+300
packages/vite-plugin-annotator/src/client/components/SelectedAnnotation.tsx
··· 1 + import { Show, createSignal } from "solid-js"; 2 + import { 3 + state, 4 + updateAnnotation, 5 + removeAnnotation, 6 + selectAnnotation, 7 + } from "../store"; 8 + 9 + /** 10 + * Panel showing details of the selected annotation 11 + */ 12 + export function SelectedAnnotation() { 13 + const selected = () => { 14 + const id = state().selectedId; 15 + if (!id) return null; 16 + return state().annotations.find((a) => a.id === id) ?? null; 17 + }; 18 + 19 + const [isEditing, setIsEditing] = createSignal(false); 20 + const [editText, setEditText] = createSignal(""); 21 + 22 + const startEdit = () => { 23 + const s = selected(); 24 + if (s) { 25 + setEditText(s.text); 26 + setIsEditing(true); 27 + } 28 + }; 29 + 30 + const saveEdit = () => { 31 + const s = selected(); 32 + if (s) { 33 + updateAnnotation(s.id, editText()); 34 + } 35 + setIsEditing(false); 36 + }; 37 + 38 + const cancelEdit = () => { 39 + setIsEditing(false); 40 + }; 41 + 42 + const handleDelete = () => { 43 + const s = selected(); 44 + if (s) { 45 + removeAnnotation(s.id); 46 + } 47 + }; 48 + 49 + return ( 50 + <Show when={selected()}> 51 + {(annotation) => ( 52 + <div 53 + data-annotator 54 + style={{ 55 + position: "fixed", 56 + bottom: "90px", 57 + right: "20px", 58 + width: "320px", 59 + "max-height": "400px", 60 + background: "white", 61 + "border-radius": "12px", 62 + "box-shadow": "0 4px 20px rgba(0,0,0,0.2)", 63 + overflow: "hidden", 64 + "z-index": "2147483646", 65 + display: "flex", 66 + "flex-direction": "column", 67 + }} 68 + > 69 + {/* Header */} 70 + <div 71 + style={{ 72 + padding: "12px 16px", 73 + "border-bottom": "1px solid #e5e7eb", 74 + display: "flex", 75 + "justify-content": "space-between", 76 + "align-items": "center", 77 + }} 78 + > 79 + <span 80 + style={{ 81 + "font-weight": "600", 82 + "font-size": "14px", 83 + color: "#1f2937", 84 + }} 85 + > 86 + Annotation #{state().annotations.indexOf(annotation()) + 1} 87 + </span> 88 + <button 89 + onClick={() => selectAnnotation(null)} 90 + style={{ 91 + background: "none", 92 + border: "none", 93 + cursor: "pointer", 94 + padding: "4px", 95 + color: "#6b7280", 96 + }} 97 + > 98 + <svg 99 + width="16" 100 + height="16" 101 + viewBox="0 0 24 24" 102 + fill="none" 103 + stroke="currentColor" 104 + stroke-width="2" 105 + > 106 + <path d="M18 6L6 18M6 6l12 12" /> 107 + </svg> 108 + </button> 109 + </div> 110 + 111 + {/* Content */} 112 + <div 113 + style={{ 114 + padding: "16px", 115 + overflow: "auto", 116 + flex: "1", 117 + }} 118 + > 119 + {/* Feedback text */} 120 + <div style={{ "margin-bottom": "16px" }}> 121 + <label 122 + style={{ 123 + "font-size": "12px", 124 + color: "#6b7280", 125 + "text-transform": "uppercase", 126 + "letter-spacing": "0.05em", 127 + }} 128 + > 129 + Feedback 130 + </label> 131 + <Show 132 + when={isEditing()} 133 + fallback={ 134 + <p 135 + style={{ 136 + "margin-top": "4px", 137 + "font-size": "14px", 138 + color: "#1f2937", 139 + cursor: "pointer", 140 + }} 141 + onClick={startEdit} 142 + title="Click to edit" 143 + > 144 + "{annotation().text}" 145 + </p> 146 + } 147 + > 148 + <input 149 + type="text" 150 + value={editText()} 151 + onInput={(e) => setEditText(e.currentTarget.value)} 152 + onKeyDown={(e) => { 153 + if (e.key === "Enter") saveEdit(); 154 + if (e.key === "Escape") cancelEdit(); 155 + }} 156 + onBlur={saveEdit} 157 + autofocus 158 + style={{ 159 + width: "100%", 160 + padding: "6px 8px", 161 + border: "1px solid #3b82f6", 162 + "border-radius": "4px", 163 + "font-size": "14px", 164 + "margin-top": "4px", 165 + "box-sizing": "border-box", 166 + }} 167 + /> 168 + </Show> 169 + </div> 170 + 171 + {/* Component info */} 172 + <Show when={annotation().element.componentName}> 173 + <div style={{ "margin-bottom": "12px" }}> 174 + <label 175 + style={{ 176 + "font-size": "12px", 177 + color: "#6b7280", 178 + "text-transform": "uppercase", 179 + "letter-spacing": "0.05em", 180 + }} 181 + > 182 + Component 183 + </label> 184 + <p 185 + style={{ 186 + "margin-top": "4px", 187 + "font-size": "13px", 188 + color: "#1f2937", 189 + "font-family": "monospace", 190 + }} 191 + > 192 + {annotation().element.componentName} 193 + <Show when={annotation().element.sourceLocation}> 194 + <span style={{ color: "#6b7280" }}> 195 + {" "} 196 + at {annotation().element.sourceLocation} 197 + </span> 198 + </Show> 199 + </p> 200 + </div> 201 + </Show> 202 + 203 + {/* Animation state */} 204 + <Show when={annotation().animations.length > 0}> 205 + <div style={{ "margin-bottom": "12px" }}> 206 + <label 207 + style={{ 208 + "font-size": "12px", 209 + color: "#6b7280", 210 + "text-transform": "uppercase", 211 + "letter-spacing": "0.05em", 212 + }} 213 + > 214 + Animations ({annotation().animations.length}) 215 + </label> 216 + <div 217 + style={{ 218 + "margin-top": "4px", 219 + "font-size": "12px", 220 + "font-family": "monospace", 221 + background: "#f3f4f6", 222 + padding: "8px", 223 + "border-radius": "4px", 224 + }} 225 + > 226 + {annotation().animations.map((anim) => ( 227 + <div style={{ "margin-bottom": "4px" }}> 228 + <span style={{ color: "#7c3aed" }}>{anim.name}</span> 229 + <span style={{ color: "#6b7280" }}> 230 + {" "} 231 + {anim.currentTime.toFixed(0)}ms /{" "} 232 + {anim.duration.toFixed(0)}ms ( 233 + {(anim.progress * 100).toFixed(0)}%) 234 + </span> 235 + </div> 236 + ))} 237 + </div> 238 + </div> 239 + </Show> 240 + 241 + {/* Element preview */} 242 + <div style={{ "margin-bottom": "12px" }}> 243 + <label 244 + style={{ 245 + "font-size": "12px", 246 + color: "#6b7280", 247 + "text-transform": "uppercase", 248 + "letter-spacing": "0.05em", 249 + }} 250 + > 251 + Element 252 + </label> 253 + <pre 254 + style={{ 255 + "margin-top": "4px", 256 + "font-size": "11px", 257 + "font-family": "monospace", 258 + background: "#f3f4f6", 259 + padding: "8px", 260 + "border-radius": "4px", 261 + overflow: "auto", 262 + "white-space": "pre-wrap", 263 + "word-break": "break-all", 264 + "max-height": "100px", 265 + }} 266 + > 267 + {annotation().element.html} 268 + </pre> 269 + </div> 270 + </div> 271 + 272 + {/* Footer */} 273 + <div 274 + style={{ 275 + padding: "12px 16px", 276 + "border-top": "1px solid #e5e7eb", 277 + display: "flex", 278 + "justify-content": "flex-end", 279 + }} 280 + > 281 + <button 282 + onClick={handleDelete} 283 + style={{ 284 + padding: "6px 12px", 285 + border: "none", 286 + "border-radius": "6px", 287 + background: "#fee2e2", 288 + color: "#dc2626", 289 + cursor: "pointer", 290 + "font-size": "13px", 291 + }} 292 + > 293 + Delete 294 + </button> 295 + </div> 296 + </div> 297 + )} 298 + </Show> 299 + ); 300 + }
+157
packages/vite-plugin-annotator/src/client/components/Toolbar.tsx
··· 1 + import { Show, createSignal } from "solid-js"; 2 + import { 3 + state, 4 + toggleActive, 5 + clearAnnotations, 6 + exportAnnotations, 7 + } from "../store"; 8 + 9 + /** 10 + * Small floating toolbar for the annotator 11 + */ 12 + export function Toolbar() { 13 + const isActive = () => state().isActive; 14 + const annotationCount = () => state().annotations.length; 15 + const [isHovered, setIsHovered] = createSignal(false); 16 + const [copied, setCopied] = createSignal(false); 17 + 18 + const showExpanded = () => isHovered() || isActive() || annotationCount() > 0; 19 + 20 + const handleExport = async () => { 21 + await exportAnnotations(); 22 + setCopied(true); 23 + setTimeout(() => setCopied(false), 2000); 24 + }; 25 + 26 + return ( 27 + <div 28 + data-annotator 29 + onMouseEnter={() => setIsHovered(true)} 30 + onMouseLeave={() => setIsHovered(false)} 31 + style={{ 32 + position: "fixed", 33 + bottom: "16px", 34 + right: "16px", 35 + "z-index": "2147483647", 36 + display: "flex", 37 + "align-items": "center", 38 + gap: "6px", 39 + padding: "6px", 40 + background: isActive() ? "rgba(59, 130, 246, 0.95)" : "rgba(24, 24, 27, 0.95)", 41 + "border-radius": "9999px", 42 + "box-shadow": "0 2px 8px rgba(0,0,0,0.2)", 43 + transition: "all 0.15s ease-out", 44 + "backdrop-filter": "blur(8px)", 45 + }} 46 + > 47 + {/* Export/Clear buttons - only when have annotations */} 48 + <Show when={showExpanded() && annotationCount() > 0}> 49 + <button 50 + onClick={handleExport} 51 + title={copied() ? "Copied!" : "Copy prompt to clipboard"} 52 + style={{ 53 + height: "28px", 54 + "padding-left": "10px", 55 + "padding-right": "10px", 56 + "border-radius": "9999px", 57 + border: "none", 58 + background: copied() ? "#10b981" : "rgba(255,255,255,0.15)", 59 + color: "white", 60 + cursor: "pointer", 61 + display: "flex", 62 + "align-items": "center", 63 + gap: "6px", 64 + "font-size": "12px", 65 + "font-weight": "500", 66 + transition: "all 0.15s", 67 + }} 68 + onMouseEnter={(e) => { 69 + if (!copied()) e.currentTarget.style.background = "rgba(255,255,255,0.25)"; 70 + }} 71 + onMouseLeave={(e) => { 72 + if (!copied()) e.currentTarget.style.background = "rgba(255,255,255,0.15)"; 73 + }} 74 + > 75 + <Show when={copied()} fallback={ 76 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 77 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2" /> 78 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /> 79 + </svg> 80 + }> 81 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 82 + <path d="M20 6L9 17l-5-5" /> 83 + </svg> 84 + </Show> 85 + {annotationCount()} 86 + </button> 87 + 88 + <button 89 + onClick={clearAnnotations} 90 + title="Clear annotations" 91 + style={{ 92 + width: "28px", 93 + height: "28px", 94 + "border-radius": "50%", 95 + border: "none", 96 + background: "rgba(239, 68, 68, 0.8)", 97 + color: "white", 98 + cursor: "pointer", 99 + display: "flex", 100 + "align-items": "center", 101 + "justify-content": "center", 102 + transition: "transform 0.1s", 103 + }} 104 + onMouseEnter={(e) => e.currentTarget.style.transform = "scale(1.1)"} 105 + onMouseLeave={(e) => e.currentTarget.style.transform = "scale(1)"} 106 + > 107 + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> 108 + <path d="M18 6L6 18M6 6l12 12" /> 109 + </svg> 110 + </button> 111 + 112 + <div style={{ width: "1px", height: "20px", background: "rgba(255,255,255,0.2)" }} /> 113 + </Show> 114 + 115 + {/* Main toggle button */} 116 + <button 117 + onClick={toggleActive} 118 + title={isActive() ? "Stop annotating (Esc)" : "Start annotating (⌘⇧A)"} 119 + style={{ 120 + width: "32px", 121 + height: "32px", 122 + "border-radius": "50%", 123 + border: "none", 124 + background: isActive() ? "white" : "transparent", 125 + color: isActive() ? "#3b82f6" : "white", 126 + cursor: "pointer", 127 + display: "flex", 128 + "align-items": "center", 129 + "justify-content": "center", 130 + transition: "all 0.15s", 131 + }} 132 + onMouseEnter={(e) => { 133 + if (!isActive()) e.currentTarget.style.background = "rgba(255,255,255,0.15)"; 134 + }} 135 + onMouseLeave={(e) => { 136 + if (!isActive()) e.currentTarget.style.background = "transparent"; 137 + }} 138 + > 139 + <Show 140 + when={isActive()} 141 + fallback={ 142 + /* Annotation/pencil icon */ 143 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 144 + <path d="M12 20h9" /> 145 + <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" /> 146 + </svg> 147 + } 148 + > 149 + {/* Check/done icon */} 150 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> 151 + <path d="M20 6L9 17l-5-5" /> 152 + </svg> 153 + </Show> 154 + </button> 155 + </div> 156 + ); 157 + }
+181
packages/vite-plugin-annotator/src/client/context.ts
··· 1 + import type { ElementContext } from "./types"; 2 + import { getSelector } from "./animations"; 3 + 4 + // bippy imports - these provide React DevTools-like fiber access 5 + let bippy: typeof import("bippy") | null = null; 6 + let bippySource: typeof import("bippy/source") | null = null; 7 + 8 + // Try to load bippy (might fail if React isn't present) 9 + async function loadBippy() { 10 + if (bippy) return; 11 + try { 12 + bippy = await import("bippy"); 13 + bippySource = await import("bippy/source"); 14 + } catch { 15 + // bippy not available, React component info won't be captured 16 + } 17 + } 18 + 19 + // Initialize bippy on load 20 + loadBippy(); 21 + 22 + /** 23 + * Get truncated outer HTML 24 + */ 25 + function getHtmlPreview(element: Element, maxLength = 500): string { 26 + const clone = element.cloneNode(true) as Element; 27 + 28 + // Truncate text content 29 + const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT); 30 + let node: Text | null; 31 + while ((node = walker.nextNode() as Text | null)) { 32 + if (node.textContent && node.textContent.length > 50) { 33 + node.textContent = node.textContent.slice(0, 50) + "..."; 34 + } 35 + } 36 + 37 + // Truncate attribute values 38 + const allElements = clone.querySelectorAll("*"); 39 + for (const el of [clone, ...Array.from(allElements)]) { 40 + for (const attr of Array.from(el.attributes)) { 41 + if (attr.value.length > 60) { 42 + el.setAttribute(attr.name, attr.value.slice(0, 60) + "..."); 43 + } 44 + } 45 + } 46 + 47 + let html = clone.outerHTML; 48 + if (html.length > maxLength) { 49 + // Find the end of the opening tag 50 + const openingTagEnd = html.indexOf(">") + 1; 51 + if (openingTagEnd > 0 && openingTagEnd < maxLength) { 52 + html = html.slice(0, maxLength) + "..."; 53 + } 54 + } 55 + 56 + return html; 57 + } 58 + 59 + /** 60 + * Get relevant computed styles 61 + */ 62 + function getRelevantStyles(element: Element): Record<string, string> { 63 + const computed = getComputedStyle(element); 64 + const relevant = [ 65 + "animation", 66 + "animation-name", 67 + "animation-duration", 68 + "animation-timing-function", 69 + "animation-delay", 70 + "animation-play-state", 71 + "transition", 72 + "transition-property", 73 + "transition-duration", 74 + "transition-timing-function", 75 + "transform", 76 + "opacity", 77 + "background-color", 78 + "color", 79 + ]; 80 + 81 + const styles: Record<string, string> = {}; 82 + for (const prop of relevant) { 83 + const value = computed.getPropertyValue(prop); 84 + if (value && value !== "none" && value !== "0s" && value !== "all 0s ease 0s") { 85 + styles[prop] = value; 86 + } 87 + } 88 + 89 + return styles; 90 + } 91 + 92 + /** 93 + * Get React component info via bippy 94 + */ 95 + async function getReactInfo( 96 + element: Element 97 + ): Promise<{ componentName?: string; sourceLocation?: string }> { 98 + if (!bippy || !bippySource) { 99 + await loadBippy(); 100 + } 101 + 102 + if (!bippy || !bippySource) { 103 + return {}; 104 + } 105 + 106 + try { 107 + if (!bippy.isInstrumentationActive()) { 108 + return {}; 109 + } 110 + 111 + const fiber = bippy.getFiberFromHostInstance(element); 112 + if (!fiber) return {}; 113 + 114 + const stack = await bippySource.getOwnerStack(fiber); 115 + if (!stack || stack.length === 0) return {}; 116 + 117 + // Find the first user component (not internal) 118 + for (const frame of stack) { 119 + if (frame.functionName && !isInternalComponent(frame.functionName)) { 120 + const sourceLocation = frame.fileName 121 + ? `${frame.fileName}:${frame.lineNumber ?? "?"}:${frame.columnNumber ?? "?"}` 122 + : undefined; 123 + 124 + return { 125 + componentName: frame.functionName, 126 + sourceLocation, 127 + }; 128 + } 129 + } 130 + 131 + return {}; 132 + } catch { 133 + return {}; 134 + } 135 + } 136 + 137 + /** 138 + * Check if component name is internal (React, Next.js, etc.) 139 + */ 140 + function isInternalComponent(name: string): boolean { 141 + const internals = [ 142 + "Suspense", 143 + "Fragment", 144 + "StrictMode", 145 + "Profiler", 146 + "InnerLayoutRouter", 147 + "OuterLayoutRouter", 148 + "RenderFromTemplateContext", 149 + "ScrollAndFocusHandler", 150 + "RedirectErrorBoundary", 151 + "NotFoundErrorBoundary", 152 + "LoadingBoundary", 153 + "ErrorBoundary", 154 + "HotReload", 155 + ]; 156 + 157 + return ( 158 + internals.includes(name) || 159 + name.startsWith("__") || 160 + name.startsWith("$") || 161 + /^[a-z]/.test(name) // lowercase = DOM element 162 + ); 163 + } 164 + 165 + /** 166 + * Capture full context for an element 167 + */ 168 + export async function captureElementContext( 169 + element: Element 170 + ): Promise<ElementContext> { 171 + const reactInfo = await getReactInfo(element); 172 + 173 + return { 174 + html: getHtmlPreview(element), 175 + selector: getSelector(element), 176 + rect: element.getBoundingClientRect(), 177 + computedStyles: getRelevantStyles(element), 178 + componentName: reactInfo.componentName, 179 + sourceLocation: reactInfo.sourceLocation, 180 + }; 181 + }
+81
packages/vite-plugin-annotator/src/client/index.tsx
··· 1 + import { render } from "solid-js/web"; 2 + import { onCleanup, onMount } from "solid-js"; 3 + import { Overlay } from "./components/Overlay"; 4 + import { Toolbar } from "./components/Toolbar"; 5 + import { SelectedAnnotation } from "./components/SelectedAnnotation"; 6 + import { state, toggleActive } from "./store"; 7 + 8 + /** 9 + * Root component that mounts everything 10 + */ 11 + function Annotator() { 12 + const handleKeyDown = (e: KeyboardEvent) => { 13 + // Cmd+Shift+A (Mac) or Ctrl+Shift+A (Windows) to toggle 14 + if (e.key.toLowerCase() === "a" && e.shiftKey && (e.metaKey || e.ctrlKey)) { 15 + e.preventDefault(); 16 + e.stopPropagation(); 17 + toggleActive(); 18 + return; 19 + } 20 + 21 + // Escape to deactivate when active 22 + if (e.key === "Escape" && state().isActive) { 23 + e.preventDefault(); 24 + e.stopPropagation(); 25 + toggleActive(); 26 + return; 27 + } 28 + }; 29 + 30 + onMount(() => { 31 + document.addEventListener("keydown", handleKeyDown, { capture: true }); 32 + console.log("[Annotator] Ready - press ⌘⇧A to start annotating"); 33 + }); 34 + 35 + onCleanup(() => { 36 + document.removeEventListener("keydown", handleKeyDown, { capture: true }); 37 + }); 38 + 39 + return ( 40 + <> 41 + <Overlay /> 42 + <Toolbar /> 43 + <SelectedAnnotation /> 44 + </> 45 + ); 46 + } 47 + 48 + /** 49 + * Mount the annotator into a shadow DOM container 50 + */ 51 + function mount() { 52 + const container = document.createElement("div"); 53 + container.id = "annotator-root"; 54 + container.setAttribute("data-annotator", ""); 55 + document.body.appendChild(container); 56 + 57 + const shadow = container.attachShadow({ mode: "open" }); 58 + 59 + const style = document.createElement("style"); 60 + style.textContent = ` 61 + :host { 62 + all: initial; 63 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 64 + } 65 + * { 66 + box-sizing: border-box; 67 + } 68 + `; 69 + shadow.appendChild(style); 70 + 71 + const mountPoint = document.createElement("div"); 72 + shadow.appendChild(mountPoint); 73 + 74 + render(() => <Annotator />, mountPoint); 75 + } 76 + 77 + if (document.readyState === "loading") { 78 + document.addEventListener("DOMContentLoaded", mount); 79 + } else { 80 + mount(); 81 + }
+144
packages/vite-plugin-annotator/src/client/prompt.ts
··· 1 + import type { Annotation, AnimationSnapshot } from "./types"; 2 + 3 + /** 4 + * Format a single animation snapshot for the prompt 5 + */ 6 + function formatAnimation(anim: AnimationSnapshot): string { 7 + const progress = (anim.progress * 100).toFixed(0); 8 + const currentMs = anim.currentTime.toFixed(0); 9 + const durationMs = anim.duration.toFixed(0); 10 + 11 + let line = ` - ${anim.name}: ${currentMs}ms / ${durationMs}ms (${progress}% complete)`; 12 + 13 + if (anim.properties?.length) { 14 + line += ` [${anim.properties.join(", ")}]`; 15 + } 16 + 17 + if (anim.easing) { 18 + line += ` easing: ${anim.easing}`; 19 + } 20 + 21 + if (anim.iterations && anim.iterations > 1) { 22 + const iterStr = 23 + anim.iterations === Infinity ? "infinite" : String(anim.iterations); 24 + line += ` iteration: ${(anim.currentIteration ?? 0) + 1}/${iterStr}`; 25 + } 26 + 27 + return line; 28 + } 29 + 30 + /** 31 + * Format a single annotation for the prompt 32 + */ 33 + function formatAnnotation(annotation: Annotation, index: number): string { 34 + const lines: string[] = []; 35 + 36 + lines.push(`### Annotation ${index + 1}`); 37 + lines.push(""); 38 + 39 + // Element info 40 + lines.push("**Element:**"); 41 + lines.push("```html"); 42 + lines.push(annotation.element.html); 43 + lines.push("```"); 44 + lines.push(""); 45 + 46 + // Component info (if available) 47 + if (annotation.element.componentName) { 48 + lines.push( 49 + `**Component:** \`${annotation.element.componentName}\`${ 50 + annotation.element.sourceLocation 51 + ? ` at \`${annotation.element.sourceLocation}\`` 52 + : "" 53 + }` 54 + ); 55 + lines.push(""); 56 + } 57 + 58 + // Selector 59 + lines.push(`**Selector:** \`${annotation.element.selector}\``); 60 + lines.push(""); 61 + 62 + // Animation state 63 + if (annotation.animations.length > 0) { 64 + lines.push("**Animation State:**"); 65 + for (const anim of annotation.animations) { 66 + lines.push(formatAnimation(anim)); 67 + } 68 + lines.push(""); 69 + } 70 + 71 + // Relevant styles 72 + const styleEntries = Object.entries(annotation.element.computedStyles); 73 + if (styleEntries.length > 0) { 74 + lines.push("**Computed Styles:**"); 75 + lines.push("```css"); 76 + for (const [prop, value] of styleEntries) { 77 + lines.push(`${prop}: ${value};`); 78 + } 79 + lines.push("```"); 80 + lines.push(""); 81 + } 82 + 83 + // User feedback 84 + lines.push(`**Feedback:** "${annotation.text}"`); 85 + lines.push(""); 86 + 87 + // Position context 88 + lines.push( 89 + `*Clicked at (${annotation.position.x.toFixed(0)}, ${annotation.position.y.toFixed(0)}) at ${new Date(annotation.timestamp).toLocaleTimeString()}*` 90 + ); 91 + 92 + return lines.join("\n"); 93 + } 94 + 95 + /** 96 + * Generate a full prompt from all annotations 97 + */ 98 + export function generatePrompt(annotations: Annotation[]): string { 99 + if (annotations.length === 0) { 100 + return "No annotations captured."; 101 + } 102 + 103 + const lines: string[] = []; 104 + 105 + lines.push("# Visual Feedback Session"); 106 + lines.push(""); 107 + lines.push( 108 + `*${annotations.length} annotation${annotations.length > 1 ? "s" : ""} captured*` 109 + ); 110 + lines.push(""); 111 + lines.push("---"); 112 + lines.push(""); 113 + 114 + // Format each annotation 115 + for (let i = 0; i < annotations.length; i++) { 116 + lines.push(formatAnnotation(annotations[i], i)); 117 + if (i < annotations.length - 1) { 118 + lines.push(""); 119 + lines.push("---"); 120 + lines.push(""); 121 + } 122 + } 123 + 124 + lines.push(""); 125 + lines.push("---"); 126 + lines.push(""); 127 + lines.push("## Instructions"); 128 + lines.push(""); 129 + lines.push( 130 + "Each annotation above captures the exact moment when feedback was given, including:" 131 + ); 132 + lines.push("- The element's HTML and CSS selector"); 133 + lines.push("- React component name and source location (if available)"); 134 + lines.push( 135 + "- Animation state at that exact moment (timing, progress, easing)" 136 + ); 137 + lines.push("- Relevant computed styles"); 138 + lines.push(""); 139 + lines.push( 140 + "Use this context to understand not just *what* needs to change, but *when* in the animation timeline the issue occurs." 141 + ); 142 + 143 + return lines.join("\n"); 144 + }
+126
packages/vite-plugin-annotator/src/client/store.ts
··· 1 + import { createSignal, createRoot } from "solid-js"; 2 + import type { Annotation, AnnotatorState } from "./types"; 3 + import { getElementAnimations } from "./animations"; 4 + import { captureElementContext } from "./context"; 5 + import { generatePrompt } from "./prompt"; 6 + 7 + // Create store in a root to ensure proper cleanup 8 + const store = createRoot(() => { 9 + const [state, setState] = createSignal<AnnotatorState>({ 10 + isActive: false, 11 + isPaused: false, 12 + annotations: [], 13 + selectedId: null, 14 + hoveredElement: null, 15 + }); 16 + 17 + return { state, setState }; 18 + }); 19 + 20 + export const { state, setState } = store; 21 + 22 + /** 23 + * Toggle annotator active state (just enables/disables click capture) 24 + */ 25 + export function toggleActive(): void { 26 + setState((s) => ({ 27 + ...s, 28 + isActive: !s.isActive, 29 + hoveredElement: null, 30 + })); 31 + } 32 + 33 + /** 34 + * Add a new annotation at the clicked element 35 + */ 36 + export async function addAnnotation( 37 + element: Element, 38 + position: { x: number; y: number }, 39 + text: string 40 + ): Promise<void> { 41 + const elementContext = await captureElementContext(element); 42 + const animations = getElementAnimations(element); 43 + 44 + const annotation: Annotation = { 45 + id: crypto.randomUUID(), 46 + text, 47 + timestamp: Date.now(), 48 + position, 49 + element: elementContext, 50 + animations, 51 + }; 52 + 53 + setState((s) => ({ 54 + ...s, 55 + annotations: [...s.annotations, annotation], 56 + selectedId: annotation.id, 57 + })); 58 + } 59 + 60 + /** 61 + * Update an annotation's text 62 + */ 63 + export function updateAnnotation(id: string, text: string): void { 64 + setState((s) => ({ 65 + ...s, 66 + annotations: s.annotations.map((a) => (a.id === id ? { ...a, text } : a)), 67 + })); 68 + } 69 + 70 + /** 71 + * Remove an annotation 72 + */ 73 + export function removeAnnotation(id: string): void { 74 + setState((s) => ({ 75 + ...s, 76 + annotations: s.annotations.filter((a) => a.id !== id), 77 + selectedId: s.selectedId === id ? null : s.selectedId, 78 + })); 79 + } 80 + 81 + /** 82 + * Clear all annotations 83 + */ 84 + export function clearAnnotations(): void { 85 + setState((s) => ({ 86 + ...s, 87 + annotations: [], 88 + selectedId: null, 89 + })); 90 + } 91 + 92 + /** 93 + * Select an annotation 94 + */ 95 + export function selectAnnotation(id: string | null): void { 96 + setState((s) => ({ 97 + ...s, 98 + selectedId: id, 99 + })); 100 + } 101 + 102 + /** 103 + * Set hovered element 104 + */ 105 + export function setHoveredElement(element: Element | null): void { 106 + setState((s) => ({ 107 + ...s, 108 + hoveredElement: element, 109 + })); 110 + } 111 + 112 + /** 113 + * Export annotations as prompt and copy to clipboard 114 + */ 115 + export async function exportAnnotations(): Promise<void> { 116 + const current = state(); 117 + const prompt = generatePrompt(current.annotations); 118 + 119 + try { 120 + await navigator.clipboard.writeText(prompt); 121 + console.log("[Annotator] Prompt copied to clipboard ✓"); 122 + } catch (err) { 123 + console.error("[Annotator] Failed to copy:", err); 124 + console.log("[Annotator] Generated prompt:\n", prompt); 125 + } 126 + }
+78
packages/vite-plugin-annotator/src/client/types.ts
··· 1 + /** 2 + * Snapshot of a single animation's state at capture time 3 + */ 4 + export interface AnimationSnapshot { 5 + /** Target element's annotator ID */ 6 + targetId: string; 7 + /** CSS selector path to element */ 8 + selector: string; 9 + /** Animation name (from @keyframes) or 'transition' */ 10 + name: string; 11 + /** Current time in milliseconds */ 12 + currentTime: number; 13 + /** Total duration in milliseconds */ 14 + duration: number; 15 + /** Progress from 0 to 1 */ 16 + progress: number; 17 + /** Play state when captured */ 18 + playState: AnimationPlayState; 19 + /** CSS properties being animated */ 20 + properties?: string[]; 21 + /** Easing function */ 22 + easing?: string; 23 + /** Number of iterations (Infinity for infinite) */ 24 + iterations?: number; 25 + /** Current iteration */ 26 + currentIteration?: number; 27 + } 28 + 29 + /** 30 + * Context captured for an annotated element 31 + */ 32 + export interface ElementContext { 33 + /** Outer HTML (truncated) */ 34 + html: string; 35 + /** CSS selector path */ 36 + selector: string; 37 + /** Bounding rect at capture time */ 38 + rect: DOMRect; 39 + /** Computed styles (subset) */ 40 + computedStyles: Record<string, string>; 41 + /** React component name (via bippy) */ 42 + componentName?: string; 43 + /** Source file location (via bippy) */ 44 + sourceLocation?: string; 45 + } 46 + 47 + /** 48 + * A single annotation with all captured context 49 + */ 50 + export interface Annotation { 51 + id: string; 52 + /** User's feedback text */ 53 + text: string; 54 + /** Timestamp when annotation was created */ 55 + timestamp: number; 56 + /** Click coordinates */ 57 + position: { x: number; y: number }; 58 + /** Element context */ 59 + element: ElementContext; 60 + /** Animation states at capture time */ 61 + animations: AnimationSnapshot[]; 62 + } 63 + 64 + /** 65 + * Annotator state 66 + */ 67 + export interface AnnotatorState { 68 + /** Is the annotator active (paused and accepting annotations) */ 69 + isActive: boolean; 70 + /** Is currently paused */ 71 + isPaused: boolean; 72 + /** All annotations */ 73 + annotations: Annotation[]; 74 + /** Currently selected annotation ID */ 75 + selectedId: string | null; 76 + /** Element being hovered while active */ 77 + hoveredElement: Element | null; 78 + }
+67
packages/vite-plugin-annotator/src/index.ts
··· 1 + import type { Plugin } from "vite"; 2 + import { readFileSync } from "node:fs"; 3 + import { resolve, dirname } from "node:path"; 4 + import { fileURLToPath } from "node:url"; 5 + 6 + const __dirname = dirname(fileURLToPath(import.meta.url)); 7 + 8 + export interface AnnotatorOptions { 9 + /** 10 + * Keyboard shortcut to toggle annotator 11 + * @default "Escape" to deactivate when active 12 + */ 13 + shortcut?: string; 14 + } 15 + 16 + const CLIENT_ID = "/@annotator-client"; 17 + 18 + export function annotator(options: AnnotatorOptions = {}): Plugin { 19 + let isDev = false; 20 + let clientCode: string | null = null; 21 + 22 + return { 23 + name: "vite-plugin-annotator", 24 + 25 + configResolved(config) { 26 + isDev = config.command === "serve"; 27 + }, 28 + 29 + configureServer(server) { 30 + // Serve the client bundle at a known URL 31 + server.middlewares.use((req, res, next) => { 32 + if (req.url === CLIENT_ID) { 33 + if (!clientCode) { 34 + const clientPath = resolve(__dirname, "client.js"); 35 + try { 36 + clientCode = readFileSync(clientPath, "utf-8"); 37 + } catch (e) { 38 + console.error("[annotator] Failed to load client:", e); 39 + clientCode = "console.error('[annotator] Client bundle not found');"; 40 + } 41 + } 42 + res.setHeader("Content-Type", "application/javascript"); 43 + res.end(clientCode); 44 + return; 45 + } 46 + next(); 47 + }); 48 + }, 49 + 50 + transformIndexHtml(html) { 51 + if (!isDev) return html; 52 + 53 + return { 54 + html, 55 + tags: [ 56 + { 57 + tag: "script", 58 + attrs: { type: "module", src: CLIENT_ID }, 59 + injectTo: "body", 60 + }, 61 + ], 62 + }; 63 + }, 64 + }; 65 + } 66 + 67 + export default annotator;
+17
packages/vite-plugin-annotator/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 6 + "jsx": "preserve", 7 + "jsxImportSource": "solid-js", 8 + "strict": true, 9 + "esModuleInterop": true, 10 + "skipLibCheck": true, 11 + "declaration": true, 12 + "outDir": "dist", 13 + "rootDir": "src", 14 + "types": ["node"] 15 + }, 16 + "include": ["src/**/*"] 17 + }
+25
packages/vite-plugin-annotator/tsup.config.ts
··· 1 + import { defineConfig } from "tsup"; 2 + import * as solidPlugin from "esbuild-plugin-solid"; 3 + 4 + export default defineConfig([ 5 + // Plugin entry (for Node.js/Vite) 6 + { 7 + entry: { index: "src/index.ts" }, 8 + format: ["esm"], 9 + dts: true, 10 + clean: true, 11 + external: ["vite"], 12 + }, 13 + // Client bundle (for browser, self-contained) 14 + { 15 + entry: { client: "src/client/index.tsx" }, 16 + format: ["esm"], 17 + platform: "browser", 18 + clean: false, 19 + noExternal: ["solid-js", "bippy"], 20 + esbuildPlugins: [solidPlugin.solidPlugin({ solid: { generate: "dom" } })], 21 + esbuildOptions(options) { 22 + options.conditions = ["browser", "solid"]; 23 + }, 24 + }, 25 + ]);