From 18b06b523637a83b82fd24761ee857c35c2c9d19 Mon Sep 17 00:00:00 2001 From: Julien Fernandez Date: Mon, 26 Jan 2026 16:16:37 +0100 Subject: [PATCH 1/7] feat: custom features (tags, MACRO, Save Markers, annotate mode) - Add ReviewTag enum with modification/verification/validation categories - Add [MACRO] flag for cross-document impact annotations - Add tag dropdown in AnnotationToolbar - Add [MACRO] toggle button with warning styling - Display tags and [MACRO] badges in AnnotationPanel - Include tags and [MACRO] in exportDiff feedback - Add Save Markers button (annotate mode) to persist validation markers - Add /api/save-markers endpoint in annotate server - Add extractValidationMarkers/injectValidationMarkers in markers.ts - Add plannotator annotate subcommand - Add /plannotator-annotate slash command - Add version tracking with planVersions Map Co-Authored-By: Claude --- apps/hook/commands/plannotator-annotate.md | 19 + apps/hook/package.json | 7 +- apps/hook/server/index.ts | 57 +- apps/marketing/package.json | 2 +- apps/portal/package.json | 2 +- apps/review/package.json | 7 +- bun.lock | 590 ++- package.json | 4 + packages/core/__tests__/parser.test.ts | 406 ++ packages/core/index.ts | 39 + packages/core/markers.ts | 284 ++ packages/core/package.json | 13 + packages/core/parser.ts | 336 ++ packages/core/types.ts | 131 + packages/editor/App.tsx | 123 +- packages/native/__tests__/hooks.test.ts | 101 + packages/native/__tests__/integration.test.ts | 282 ++ .../native/components/AnnotationPanel.tsx | 288 ++ .../native/components/AnnotationToolbar.tsx | 280 ++ packages/native/components/BlockRenderer.tsx | 318 ++ packages/native/components/CodeBlock.tsx | 120 + packages/native/components/InlineMarkdown.tsx | 153 + packages/native/components/PlanViewer.tsx | 230 + .../native/components/PlannotatorModal.tsx | 372 ++ packages/native/hooks/index.ts | 12 + packages/native/hooks/useAnnotations.ts | 86 + packages/native/hooks/usePlanReview.ts | 150 + packages/native/hooks/useTextSelection.ts | 163 + packages/native/index.ts | 42 + packages/native/package.json | 19 + packages/native/types.ts | 70 + packages/server/annotate.ts | 258 + packages/server/index.ts | 33 +- packages/server/package.json | 4 + packages/server/project.test.ts | 6 +- packages/server/storage.ts | 2 +- packages/ui/components/AnnotationPanel.tsx | 18 +- packages/ui/components/AnnotationToolbar.tsx | 119 +- packages/ui/components/Viewer.tsx | 89 +- packages/ui/package.json | 1 + packages/ui/types.ts | 101 +- packages/ui/utils/markers.ts | 15 + packages/ui/utils/parser.ts | 328 +- packages/ui/utils/sharing.ts | 35 +- pnpm-lock.yaml | 4336 +++++++++++++++++ pnpm-workspace.yaml | 3 + test-macro.md | 32 + 47 files changed, 9612 insertions(+), 474 deletions(-) create mode 100644 apps/hook/commands/plannotator-annotate.md create mode 100644 packages/core/__tests__/parser.test.ts create mode 100644 packages/core/index.ts create mode 100644 packages/core/markers.ts create mode 100644 packages/core/package.json create mode 100644 packages/core/parser.ts create mode 100644 packages/core/types.ts create mode 100644 packages/native/__tests__/hooks.test.ts create mode 100644 packages/native/__tests__/integration.test.ts create mode 100644 packages/native/components/AnnotationPanel.tsx create mode 100644 packages/native/components/AnnotationToolbar.tsx create mode 100644 packages/native/components/BlockRenderer.tsx create mode 100644 packages/native/components/CodeBlock.tsx create mode 100644 packages/native/components/InlineMarkdown.tsx create mode 100644 packages/native/components/PlanViewer.tsx create mode 100644 packages/native/components/PlannotatorModal.tsx create mode 100644 packages/native/hooks/index.ts create mode 100644 packages/native/hooks/useAnnotations.ts create mode 100644 packages/native/hooks/usePlanReview.ts create mode 100644 packages/native/hooks/useTextSelection.ts create mode 100644 packages/native/index.ts create mode 100644 packages/native/package.json create mode 100644 packages/native/types.ts create mode 100644 packages/server/annotate.ts create mode 100644 packages/ui/utils/markers.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 test-macro.md diff --git a/apps/hook/commands/plannotator-annotate.md b/apps/hook/commands/plannotator-annotate.md new file mode 100644 index 0000000..fa5fefd --- /dev/null +++ b/apps/hook/commands/plannotator-annotate.md @@ -0,0 +1,19 @@ +--- +description: Annotate any markdown file with visual feedback +allowed-tools: Bash(plannotator:*) +--- + +## Markdown Annotation Feedback + +!`plannotator annotate $ARGUMENTS` + +## Your task + +Address the feedback above. The user has reviewed the markdown file and provided specific annotations: + +- **DELETION**: Remove the specified text +- **REPLACEMENT**: Change text from X to Y exactly as indicated +- **COMMENT**: Consider this feedback (may or may not require changes) +- **INSERTION**: Add new content at the specified location + +Apply the requested changes to the file. diff --git a/apps/hook/package.json b/apps/hook/package.json index ccf9c71..f7f1289 100644 --- a/apps/hook/package.json +++ b/apps/hook/package.json @@ -18,10 +18,11 @@ "@tailwindcss/vite": "^4.1.18" }, "devDependencies": { - "@vitejs/plugin-react": "^5.0.0", + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "4.3.4", + "micromatch": "^4.0.8", "typescript": "~5.8.2", "vite": "^6.2.0", - "vite-plugin-singlefile": "^2.0.3", - "@types/node": "^22.14.0" + "vite-plugin-singlefile": "^2.0.3" } } diff --git a/apps/hook/server/index.ts b/apps/hook/server/index.ts index 2b3e60c..4a1031a 100644 --- a/apps/hook/server/index.ts +++ b/apps/hook/server/index.ts @@ -1,7 +1,7 @@ /** * Plannotator CLI for Claude Code * - * Supports two modes: + * Supports three modes: * * 1. Plan Review (default, no args): * - Spawned by ExitPlanMode hook @@ -9,15 +9,21 @@ * - Serves UI, returns approve/deny decision to stdout * * 2. Code Review (`plannotator review`): - * - Triggered by /review slash command + * - Triggered by /plannotator-review slash command * - Runs git diff, opens review UI * - Outputs feedback to stdout (captured by slash command) * + * 3. Markdown Annotation (`plannotator annotate `): + * - Triggered by /plannotator-annotate slash command + * - Opens any markdown file for annotation + * - Outputs feedback to stdout for Claude to apply corrections + * * Environment variables: * PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (preferred) * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) */ +import path from "path"; import { startPlannotatorServer, handleServerReady, @@ -26,6 +32,7 @@ import { startReviewServer, handleReviewServerReady, } from "@plannotator/server/review"; +import { startAnnotateServer } from "@plannotator/server/annotate"; import { getGitContext, runGitDiff } from "@plannotator/server/git"; // Embed the built HTML at compile time @@ -82,6 +89,52 @@ if (args[0] === "review") { console.log(result.feedback || "No feedback provided."); process.exit(0); +} else if (args[0] === "annotate") { + // ============================================ + // MARKDOWN ANNOTATION MODE + // ============================================ + + const filePath = args[1]; + if (!filePath) { + console.error("Usage: plannotator annotate "); + process.exit(1); + } + + // Resolve path + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath); + + // Read file + const file = Bun.file(absolutePath); + if (!(await file.exists())) { + console.error(`File not found: ${absolutePath}`); + process.exit(1); + } + const markdown = await file.text(); + + // Start annotation server (reuses plan UI) + const server = await startAnnotateServer({ + markdown, + filePath: absolutePath, + origin: "claude-code", + sharingEnabled, + htmlContent: planHtmlContent, + onReady: handleServerReady, + }); + + const result = await server.waitForDecision(); + + // Give browser time to receive response and update UI + await Bun.sleep(1500); + + // Cleanup + server.stop(); + + // Output feedback markdown (captured by slash command) + console.log(result.feedback || "No annotations provided."); + process.exit(0); + } else { // ============================================ // PLAN REVIEW MODE (default) diff --git a/apps/marketing/package.json b/apps/marketing/package.json index e119ade..7e9faab 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -17,7 +17,7 @@ "@tailwindcss/vite": "^4.1.18" }, "devDependencies": { - "@vitejs/plugin-react": "^5.0.0", + "@vitejs/plugin-react": "4.3.4", "typescript": "~5.8.2", "vite": "^6.2.0", "@types/node": "^22.14.0" diff --git a/apps/portal/package.json b/apps/portal/package.json index 0dc8576..0839af5 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -17,7 +17,7 @@ "@tailwindcss/vite": "^4.1.18" }, "devDependencies": { - "@vitejs/plugin-react": "^5.0.0", + "@vitejs/plugin-react": "4.3.4", "typescript": "~5.8.2", "vite": "^6.2.0", "@types/node": "^22.14.0" diff --git a/apps/review/package.json b/apps/review/package.json index c37acc6..a45b7ee 100644 --- a/apps/review/package.json +++ b/apps/review/package.json @@ -19,10 +19,11 @@ "@tailwindcss/vite": "^4.1.18" }, "devDependencies": { - "@vitejs/plugin-react": "^5.0.0", + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "4.3.4", + "micromatch": "^4.0.8", "typescript": "~5.8.2", "vite": "^6.2.0", - "vite-plugin-singlefile": "^2.0.3", - "@types/node": "^22.14.0" + "vite-plugin-singlefile": "^2.0.3" } } diff --git a/bun.lock b/bun.lock index 13ff972..c28dfdc 100644 --- a/bun.lock +++ b/bun.lock @@ -1,11 +1,16 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "plannotator", "dependencies": { "@pierre/diffs": "^1.0.4", }, + "devDependencies": { + "micromatch": "^4.0.8", + "typescript": "^5.9.3", + }, }, "apps/hook": { "name": "@plannotator/hooks", @@ -21,7 +26,8 @@ }, "devDependencies": { "@types/node": "^22.14.0", - "@vitejs/plugin-react": "^5.0.0", + "@vitejs/plugin-react": "4.3.4", + "micromatch": "^4.0.8", "typescript": "~5.8.2", "vite": "^6.2.0", "vite-plugin-singlefile": "^2.0.3", @@ -40,14 +46,14 @@ }, "devDependencies": { "@types/node": "^22.14.0", - "@vitejs/plugin-react": "^5.0.0", + "@vitejs/plugin-react": "4.3.4", "typescript": "~5.8.2", "vite": "^6.2.0", }, }, "apps/opencode-plugin": { "name": "@plannotator/opencode", - "version": "0.4.16", + "version": "0.6.4", "dependencies": { "@opencode-ai/plugin": "^1.0.218", }, @@ -68,7 +74,7 @@ }, "devDependencies": { "@types/node": "^22.14.0", - "@vitejs/plugin-react": "^5.0.0", + "@vitejs/plugin-react": "4.3.4", "typescript": "~5.8.2", "vite": "^6.2.0", }, @@ -88,12 +94,17 @@ }, "devDependencies": { "@types/node": "^22.14.0", - "@vitejs/plugin-react": "^5.0.0", + "@vitejs/plugin-react": "4.3.4", + "micromatch": "^4.0.8", "typescript": "~5.8.2", "vite": "^6.2.0", "vite-plugin-singlefile": "^2.0.3", }, }, + "packages/core": { + "name": "@plannotator/core", + "version": "0.1.0", + }, "packages/editor": { "name": "@plannotator/editor", "version": "0.0.1", @@ -104,6 +115,17 @@ "tailwindcss": "^4.1.18", }, }, + "packages/native": { + "name": "@plannotator/native", + "version": "0.1.0", + "dependencies": { + "@plannotator/core": "workspace:*", + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-native": ">=0.70.0", + }, + }, "packages/review-editor": { "name": "@plannotator/review-editor", "version": "0.0.1", @@ -117,7 +139,10 @@ }, "packages/server": { "name": "@plannotator/server", - "version": "0.4.16", + "version": "0.6.4", + "dependencies": { + "@plannotator/core": "workspace:*", + }, "peerDependencies": { "bun": ">=1.0.0", }, @@ -126,6 +151,7 @@ "name": "@plannotator/ui", "version": "0.0.1", "dependencies": { + "@plannotator/core": "workspace:*", "highlight.js": "^11.11.1", "perfect-freehand": "^1.2.2", "react": "^19.2.3", @@ -164,14 +190,48 @@ "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], + + "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], + + "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], + + "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw=="], + + "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], + + "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], + + "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], + + "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], + + "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], + + "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], + + "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], + + "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], + + "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="], + + "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/traverse--for-generate-function-map": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -226,12 +286,32 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="], + + "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="], + + "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], + + "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], + + "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + "@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=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@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=="], @@ -264,12 +344,16 @@ "@pierre/diffs": ["@pierre/diffs@1.0.4", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-jH0uzP3syMFcz0rp4uDDGTkV8sbMeh0W8Gq+e3P1rIiJ+gdGISfG+Qhwpe//W2W5Ac9G5vBAbMflywz+lLaJlQ=="], + "@plannotator/core": ["@plannotator/core@workspace:packages/core"], + "@plannotator/editor": ["@plannotator/editor@workspace:packages/editor"], "@plannotator/hooks": ["@plannotator/hooks@workspace:apps/hook"], "@plannotator/marketing": ["@plannotator/marketing@workspace:apps/marketing"], + "@plannotator/native": ["@plannotator/native@workspace:packages/native"], + "@plannotator/opencode": ["@plannotator/opencode@workspace:apps/opencode-plugin"], "@plannotator/portal": ["@plannotator/portal@workspace:apps/portal"], @@ -282,7 +366,25 @@ "@plannotator/ui": ["@plannotator/ui@workspace:packages/ui"], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + "@react-native/assets-registry": ["@react-native/assets-registry@0.83.1", "", {}, "sha512-AT7/T6UwQqO39bt/4UL5EXvidmrddXrt0yJa7ENXndAv+8yBzMsZn6fyiax6+ERMt9GLzAECikv3lj22cn2wJA=="], + + "@react-native/codegen": ["@react-native/codegen@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-FpRxenonwH+c2a5X5DZMKUD7sCudHxB3eSQPgV9R+uxd28QWslyAWrpnJM/Az96AEksHnymDzEmzq2HLX5nb+g=="], + + "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.83.1", "", { "dependencies": { "@react-native/dev-middleware": "0.83.1", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.3", "metro-config": "^0.83.3", "metro-core": "^0.83.3", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-FqR1ftydr08PYlRbrDF06eRiiiGOK/hNmz5husv19sK6iN5nHj1SMaCIVjkH/a5vryxEddyFhU6PzO/uf4kOHg=="], + + "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.83.1", "", {}, "sha512-01Rn3goubFvPjHXONooLmsW0FLxJDKIUJNOlOS0cPtmmTIx9YIjxhe/DxwHXGk7OnULd7yl3aYy7WlBsEd5Xmg=="], + + "@react-native/debugger-shell": ["@react-native/debugger-shell@0.83.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "fb-dotslash": "0.5.8" } }, "sha512-d+0w446Hxth5OP/cBHSSxOEpbj13p2zToUy6e5e3tTERNJ8ueGlW7iGwGTrSymNDgXXFjErX+dY4P4/3WokPIQ=="], + + "@react-native/dev-middleware": ["@react-native/dev-middleware@0.83.1", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.83.1", "@react-native/debugger-shell": "0.83.1", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-QJaSfNRzj3Lp7MmlCRgSBlt1XZ38xaBNXypXAp/3H3OdFifnTZOeYOpFmcpjcXYnDqkxetuwZg8VL65SQhB8dg=="], + + "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.83.1", "", {}, "sha512-6ESDnwevp1CdvvxHNgXluil5OkqbjkJAkVy7SlpFsMGmVhrSxNAgD09SSRxMNdKsnLtzIvMsFCzyHLsU/S4PtQ=="], + + "@react-native/js-polyfills": ["@react-native/js-polyfills@0.83.1", "", {}, "sha512-qgPpdWn/c5laA+3WoJ6Fak8uOm7CG50nBsLlPsF8kbT7rUHIVB9WaP6+GPsoKV/H15koW7jKuLRoNVT7c3Ht3w=="], + + "@react-native/normalize-colors": ["@react-native/normalize-colors@0.83.1", "", {}, "sha512-84feABbmeWo1kg81726UOlMKAhcQyFXYz2SjRKYkS78QmfhVDhJ2o/ps1VjhFfBz0i/scDwT1XNv9GwmRIghkg=="], + + "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.83.1", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-MdmoAbQUTOdicCocm5XAFDJWsswxk7hxa6ALnm6Y88p01HFML0W593hAn6qOt9q6IM1KbAcebtH6oOd4gcQy8w=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], @@ -344,6 +446,12 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], + + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], @@ -384,84 +492,270 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + + "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], + + "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], + + "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], + + "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="], + + "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], + + "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "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=="], + "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun": ["bun@1.3.5", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.5", "@oven/bun-darwin-x64": "1.3.5", "@oven/bun-darwin-x64-baseline": "1.3.5", "@oven/bun-linux-aarch64": "1.3.5", "@oven/bun-linux-aarch64-musl": "1.3.5", "@oven/bun-linux-x64": "1.3.5", "@oven/bun-linux-x64-baseline": "1.3.5", "@oven/bun-linux-x64-musl": "1.3.5", "@oven/bun-linux-x64-musl-baseline": "1.3.5", "@oven/bun-windows-x64": "1.3.5", "@oven/bun-windows-x64-baseline": "1.3.5" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-c1YHIGUfgvYPJmLug5QiLzNWlX2Dg7X/67JWu1Va+AmMXNXzC/KQn2lgQ7rD+n1u1UqDpJMowVGGxTNpbPydNw=="], + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], + + "chromium-edge-launcher": ["chromium-edge-launcher@0.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg=="], + + "ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], + "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=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="], + + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "hermes-compiler": ["hermes-compiler@0.14.0", "", {}, "sha512-clxa193o+GYYwykWVFfpHduCATz8fR5jvU7ngXpfKHj+E9hr9vjLNtdLSEe8MUbObvVexV3wcyxQ00xTPIrB1Q=="], + + "hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + + "hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + + "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], + + "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], + + "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], + + "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], + + "jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], + + "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + + "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + + "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], @@ -486,14 +780,56 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], + + "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "metro": ["metro@0.83.3", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.32.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q=="], + + "metro-babel-transformer": ["metro-babel-transformer@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g=="], + + "metro-cache": ["metro-cache@0.83.3", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.3" } }, "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q=="], + + "metro-cache-key": ["metro-cache-key@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw=="], + + "metro-config": ["metro-config@0.83.3", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.3", "metro-cache": "0.83.3", "metro-core": "0.83.3", "metro-runtime": "0.83.3", "yaml": "^2.6.1" } }, "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA=="], + + "metro-core": ["metro-core@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.3" } }, "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw=="], + + "metro-file-map": ["metro-file-map@0.83.3", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA=="], + + "metro-minify-terser": ["metro-minify-terser@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ=="], + + "metro-resolver": ["metro-resolver@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ=="], + + "metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="], + + "metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="], + + "metro-symbolicate": ["metro-symbolicate@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw=="], + + "metro-transform-plugins": ["metro-transform-plugins@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A=="], + + "metro-transform-worker": ["metro-transform-worker@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.83.3", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-minify-terser": "0.83.3", "metro-source-map": "0.83.3", "metro-transform-plugins": "0.83.3", "nullthrows": "^1.1.1" } }, "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], @@ -506,31 +842,89 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + + "ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], + + "on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "perfect-freehand": ["perfect-freehand@1.2.2", "", {}, "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], "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=="], + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], - "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-native": ["react-native@0.83.1", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.83.1", "@react-native/codegen": "0.83.1", "@react-native/community-cli-plugin": "0.83.1", "@react-native/gradle-plugin": "0.83.1", "@react-native/js-polyfills": "0.83.1", "@react-native/normalize-colors": "0.83.1", "@react-native/virtualized-lists": "0.83.1", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "hermes-compiler": "0.14.0", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.3", "metro-source-map": "^0.83.3", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-mL1q5HPq5cWseVhWRLl+Fwvi5z1UO+3vGOpjr+sHFwcUletPRZ5Kv+d0tUfqHmvi73/53NjlQqX1Pyn4GguUfA=="], + + "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], + + "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], @@ -538,31 +932,89 @@ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="], + + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], "shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + + "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], + + "statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], + + "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=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], + + "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + + "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], + + "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -578,8 +1030,12 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "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=="], + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], @@ -588,14 +1044,78 @@ "vite-plugin-singlefile": ["vite-plugin-singlefile@2.3.0", "", { "dependencies": { "micromatch": "^4.0.8" }, "peerDependencies": { "rollup": "^4.44.1", "vite": "^5.4.11 || ^6.0.0 || ^7.0.0" } }, "sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A=="], + "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], + + "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + "web-highlighter": ["web-highlighter@0.7.4", "", {}, "sha512-07mWw6ib+Abcr/fuKEWYPy1Y0MsCOz6yUUVw9xMpyt5UzyXWtSlcwzy2YqnccCP/LjtM9MZHQSo3dbGJII8h6w=="], + "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "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=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + + "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-syntax-async-generators/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-bigint/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-class-properties/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-class-static-block/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-import-attributes/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-import-meta/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-logical-assignment-operators/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-numeric-separator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-object-rest-spread/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-optional-catch-binding/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-optional-chaining/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-private-property-in-object/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-top-level-await/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "@plannotator/hooks/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "@plannotator/marketing/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "@plannotator/portal/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "@plannotator/review/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], @@ -608,6 +1128,50 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "send/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], } } diff --git a/package.json b/package.json index e9caacf..d974cf2 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,9 @@ }, "dependencies": { "@pierre/diffs": "^1.0.4" + }, + "devDependencies": { + "micromatch": "^4.0.8", + "typescript": "^5.9.3" } } diff --git a/packages/core/__tests__/parser.test.ts b/packages/core/__tests__/parser.test.ts new file mode 100644 index 0000000..36ca939 --- /dev/null +++ b/packages/core/__tests__/parser.test.ts @@ -0,0 +1,406 @@ +/** + * Tests for @plannotator/core parser + */ + +import { describe, test, expect } from 'bun:test'; +import { parseMarkdownToBlocks, exportDiff, extractFrontmatter, Frontmatter } from '../parser'; +import { Annotation, AnnotationType, Block } from '../types'; + +describe('extractFrontmatter', () => { + test('extracts simple frontmatter', () => { + const markdown = `--- +title: My Plan +author: Claude +--- + +# Content`; + + const result = extractFrontmatter(markdown); + + expect(result.frontmatter).not.toBeNull(); + expect(result.frontmatter?.title).toBe('My Plan'); + expect(result.frontmatter?.author).toBe('Claude'); + expect(result.content.trim()).toBe('# Content'); + }); + + test('handles array values in frontmatter', () => { + const markdown = `--- +tags: + - javascript + - typescript +--- + +Content`; + + const result = extractFrontmatter(markdown); + + expect(result.frontmatter?.tags).toEqual(['javascript', 'typescript']); + }); + + test('returns null frontmatter when not present', () => { + const markdown = `# No Frontmatter + +Just content`; + + const result = extractFrontmatter(markdown); + + expect(result.frontmatter).toBeNull(); + expect(result.content).toBe(markdown); + }); + + test('handles unclosed frontmatter', () => { + const markdown = `--- +title: Unclosed + +Content without closing`; + + const result = extractFrontmatter(markdown); + + expect(result.frontmatter).toBeNull(); + }); +}); + +describe('parseMarkdownToBlocks', () => { + test('parses headings with correct levels', () => { + const markdown = `# Heading 1 +## Heading 2 +### Heading 3`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(3); + expect(blocks[0].type).toBe('heading'); + expect(blocks[0].level).toBe(1); + expect(blocks[0].content).toBe('Heading 1'); + expect(blocks[1].level).toBe(2); + expect(blocks[2].level).toBe(3); + }); + + test('parses paragraphs', () => { + const markdown = `First paragraph. + +Second paragraph.`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(2); + expect(blocks[0].type).toBe('paragraph'); + expect(blocks[0].content).toBe('First paragraph.'); + expect(blocks[1].content).toBe('Second paragraph.'); + }); + + test('parses code blocks with language', () => { + const markdown = `\`\`\`typescript +const x: number = 42; +console.log(x); +\`\`\``; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(1); + expect(blocks[0].type).toBe('code'); + expect(blocks[0].language).toBe('typescript'); + expect(blocks[0].content).toBe('const x: number = 42;\nconsole.log(x);'); + }); + + test('parses code blocks without language', () => { + const markdown = `\`\`\` +plain code +\`\`\``; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks[0].language).toBeUndefined(); + }); + + test('parses list items with correct indentation', () => { + const markdown = `- Item 1 + - Nested item + - Double nested +- Item 2`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(4); + expect(blocks[0].type).toBe('list-item'); + expect(blocks[0].level).toBe(0); + expect(blocks[1].level).toBe(1); + expect(blocks[2].level).toBe(2); + expect(blocks[3].level).toBe(0); + }); + + test('parses checkboxes', () => { + const markdown = `- [x] Done task +- [ ] Pending task +- Regular item`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks[0].checked).toBe(true); + expect(blocks[0].content).toBe('Done task'); + expect(blocks[1].checked).toBe(false); + expect(blocks[1].content).toBe('Pending task'); + expect(blocks[2].checked).toBeUndefined(); + }); + + test('parses blockquotes', () => { + const markdown = `> This is a quote +> With multiple lines`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(2); + expect(blocks[0].type).toBe('blockquote'); + expect(blocks[0].content).toBe('This is a quote'); + }); + + test('parses horizontal rules', () => { + const markdown = `Content before + +--- + +Content after`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(3); + expect(blocks[1].type).toBe('hr'); + }); + + test('parses tables', () => { + const markdown = `| Col 1 | Col 2 | +|-------|-------| +| A | B | +| C | D |`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(1); + expect(blocks[0].type).toBe('table'); + expect(blocks[0].content).toContain('Col 1'); + }); + + test('tracks start line numbers', () => { + const markdown = `# Heading + +Paragraph + +- List item`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks[0].startLine).toBe(1); + expect(blocks[1].startLine).toBe(3); + expect(blocks[2].startLine).toBe(5); + }); + + test('generates unique block IDs', () => { + const markdown = `# One +# Two +# Three`; + + const blocks = parseMarkdownToBlocks(markdown); + const ids = blocks.map(b => b.id); + const uniqueIds = new Set(ids); + + expect(uniqueIds.size).toBe(ids.length); + }); + + test('strips frontmatter before parsing', () => { + const markdown = `--- +title: Test +--- + +# Actual Content`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(1); + expect(blocks[0].type).toBe('heading'); + expect(blocks[0].content).toBe('Actual Content'); + }); + + test('handles numbered lists', () => { + const markdown = `1. First +2. Second +3. Third`; + + const blocks = parseMarkdownToBlocks(markdown); + + expect(blocks).toHaveLength(3); + expect(blocks[0].type).toBe('list-item'); + expect(blocks[0].content).toBe('First'); + }); +}); + +describe('exportDiff', () => { + const createBlock = (id: string, content: string): Block => ({ + id, + type: 'paragraph', + content, + order: 1, + startLine: 1, + }); + + const createAnnotation = ( + id: string, + blockId: string, + type: AnnotationType, + originalText: string, + text?: string + ): Annotation => ({ + id, + blockId, + startOffset: 0, + endOffset: originalText.length, + type, + originalText, + text, + createdAt: Date.now(), + }); + + test('returns "No changes" for empty annotations', () => { + const blocks = [createBlock('b1', 'Hello')]; + const result = exportDiff(blocks, []); + + expect(result).toBe('No changes detected.'); + }); + + test('exports deletion annotations', () => { + const blocks = [createBlock('b1', 'Hello world')]; + const annotations = [ + createAnnotation('a1', 'b1', AnnotationType.DELETION, 'Hello'), + ]; + + const result = exportDiff(blocks, annotations); + + expect(result).toContain('# Plan Feedback'); + expect(result).toContain('Remove this'); + expect(result).toContain('Hello'); + expect(result).toContain("I don't want this"); + }); + + test('exports comment annotations', () => { + const blocks = [createBlock('b1', 'Some text')]; + const annotations = [ + createAnnotation('a1', 'b1', AnnotationType.COMMENT, 'Some', 'This needs clarification'), + ]; + + const result = exportDiff(blocks, annotations); + + expect(result).toContain('Feedback on'); + expect(result).toContain('Some'); + expect(result).toContain('This needs clarification'); + }); + + test('exports replacement annotations', () => { + const blocks = [createBlock('b1', 'Old text')]; + const annotations = [ + createAnnotation('a1', 'b1', AnnotationType.REPLACEMENT, 'Old', 'New'), + ]; + + const result = exportDiff(blocks, annotations); + + expect(result).toContain('Change this'); + expect(result).toContain('From:'); + expect(result).toContain('Old'); + expect(result).toContain('To:'); + expect(result).toContain('New'); + }); + + test('exports insertion annotations', () => { + const blocks = [createBlock('b1', 'Text')]; + const annotations = [ + createAnnotation('a1', 'b1', AnnotationType.INSERTION, '', 'New content'), + ]; + + const result = exportDiff(blocks, annotations); + + expect(result).toContain('Add this'); + expect(result).toContain('New content'); + }); + + test('exports global comment annotations', () => { + const blocks = [createBlock('b1', 'Text')]; + const annotations = [ + createAnnotation('a1', '', AnnotationType.GLOBAL_COMMENT, '', 'Overall feedback'), + ]; + + const result = exportDiff(blocks, annotations); + + expect(result).toContain('General feedback'); + expect(result).toContain('Overall feedback'); + }); + + test('includes global attachments', () => { + const blocks = [createBlock('b1', 'Text')]; + const globalAttachments = ['/path/to/image1.png', '/path/to/image2.jpg']; + + const result = exportDiff(blocks, [], globalAttachments); + + expect(result).toContain('Reference Images'); + expect(result).toContain('/path/to/image1.png'); + expect(result).toContain('/path/to/image2.jpg'); + }); + + test('includes annotation image paths', () => { + const blocks = [createBlock('b1', 'Text')]; + const annotations: Annotation[] = [{ + id: 'a1', + blockId: 'b1', + startOffset: 0, + endOffset: 4, + type: AnnotationType.COMMENT, + originalText: 'Text', + text: 'See attached', + createdAt: Date.now(), + imagePaths: ['/path/to/screenshot.png'], + }]; + + const result = exportDiff(blocks, annotations); + + expect(result).toContain('Attached images'); + expect(result).toContain('/path/to/screenshot.png'); + }); + + test('sorts annotations by block order and offset', () => { + const blocks = [ + { ...createBlock('b1', 'First'), order: 1 }, + { ...createBlock('b2', 'Second'), order: 2 }, + ]; + const annotations = [ + { ...createAnnotation('a2', 'b2', AnnotationType.COMMENT, 'Second', 'Comment B'), startOffset: 0 }, + { ...createAnnotation('a1', 'b1', AnnotationType.COMMENT, 'First', 'Comment A'), startOffset: 0 }, + ]; + + const result = exportDiff(blocks, annotations); + + const indexA = result.indexOf('Comment A'); + const indexB = result.indexOf('Comment B'); + expect(indexA).toBeLessThan(indexB); + }); + + test('counts annotations correctly', () => { + const blocks = [createBlock('b1', 'Text')]; + const annotations = [ + createAnnotation('a1', 'b1', AnnotationType.COMMENT, 'T', 'One'), + createAnnotation('a2', 'b1', AnnotationType.COMMENT, 'e', 'Two'), + createAnnotation('a3', 'b1', AnnotationType.COMMENT, 'x', 'Three'), + ]; + + const result = exportDiff(blocks, annotations); + + expect(result).toContain('3 pieces of feedback'); + }); + + test('uses singular for single annotation', () => { + const blocks = [createBlock('b1', 'Text')]; + const annotations = [ + createAnnotation('a1', 'b1', AnnotationType.COMMENT, 'Text', 'Single'), + ]; + + const result = exportDiff(blocks, annotations); + + expect(result).toContain('1 piece of feedback'); + }); +}); diff --git a/packages/core/index.ts b/packages/core/index.ts new file mode 100644 index 0000000..a9ef221 --- /dev/null +++ b/packages/core/index.ts @@ -0,0 +1,39 @@ +/** + * @plannotator/core + * Core logic for Plannotator - portable across platforms (web, React Native, Node) + */ + +// Types +export { + AnnotationType, + ReviewTag, + REVIEW_TAG_CATEGORIES, + type EditorMode, + type Annotation, + type Block, + type DiffResult, + type CodeAnnotationType, + type CodeAnnotation, + type DiffAnnotationMetadata, + type SelectedLineRange, +} from './types'; + +// Parser +export { + parseMarkdownToBlocks, + exportDiff, + extractFrontmatter, + type Frontmatter, +} from './parser'; + +// Validation Markers +export { + extractValidationMarkers, + injectValidationMarkers, + stripValidationMarkers, + formatMarkerForDisplay, + isValidationTag, + countValidationAnnotations, + type ValidationMarker, + type MarkerInjectionResult, +} from './markers'; diff --git a/packages/core/markers.ts b/packages/core/markers.ts new file mode 100644 index 0000000..18326bb --- /dev/null +++ b/packages/core/markers.ts @@ -0,0 +1,284 @@ +/** + * @plannotator/core - Validation Markers + * + * Functions to inject and extract validation markers (HTML comments) in markdown. + * These markers persist across sessions so Claude knows which sections are validated. + * + * Format: or or + * With metadata: + */ + +import { type Annotation, ReviewTag, REVIEW_TAG_CATEGORIES } from './types'; + +// --- Types --- + +export interface ValidationMarker { + /** The validation tag (@OK, @APPROVED, @LOCKED) */ + tag: ReviewTag; + /** Position in the markdown where this marker appears */ + position: number; + /** Line number (1-based) where the marker appears */ + line: number; + /** The text/heading this marker applies to (if context can be determined) */ + context?: string; + /** Optional attributes (author, date, reason, etc.) */ + attributes?: Record; +} + +export interface MarkerInjectionResult { + /** The modified markdown with markers injected */ + markdown: string; + /** Number of markers added */ + markersAdded: number; + /** Details about each marker added */ + markers: Array<{ tag: ReviewTag; context: string; line: number }>; +} + +// --- Constants --- + +/** Tags that can be persisted as validation markers */ +const VALIDATION_TAGS = REVIEW_TAG_CATEGORIES.validation; + +/** Regex to match validation markers in markdown */ +const MARKER_REGEX = //g; + +/** Regex to match a specific marker with all attributes */ +const MARKER_WITH_ATTRS_REGEX = //; + +// --- Extraction --- + +/** + * Extract all validation markers from markdown content. + * + * @param markdown - The markdown content to scan + * @returns Array of validation markers found + * + * @example + * ```typescript + * const markers = extractValidationMarkers(markdown); + * // => [{ tag: '@APPROVED', position: 245, line: 12, context: 'Step 1: Initialize' }] + * ``` + */ +export function extractValidationMarkers(markdown: string): ValidationMarker[] { + const markers: ValidationMarker[] = []; + const lines = markdown.split('\n'); + + let charOffset = 0; + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + const match = line.match(MARKER_WITH_ATTRS_REGEX); + + if (match) { + const tag = match[1] as ReviewTag; + const attrsString = match[2] || ''; + + // Parse attributes + const attributes: Record = {}; + const attrMatches = attrsString.matchAll(/(\w+)="([^"]*)"/g); + for (const attrMatch of attrMatches) { + attributes[attrMatch[1]] = attrMatch[2]; + } + + // Find context (look at previous non-empty line for heading/content) + let context: string | undefined; + for (let i = lineIndex - 1; i >= 0; i--) { + const prevLine = lines[i].trim(); + if (prevLine) { + // Extract heading text if it's a heading + const headingMatch = prevLine.match(/^#{1,6}\s+(.+)$/); + context = headingMatch ? headingMatch[1] : prevLine.slice(0, 50); + break; + } + } + + markers.push({ + tag, + position: charOffset + line.indexOf(match[0]), + line: lineIndex + 1, + context, + attributes: Object.keys(attributes).length > 0 ? attributes : undefined, + }); + } + + charOffset += line.length + 1; // +1 for newline + } + + return markers; +} + +/** + * Check if a specific tag already exists near a given text context. + */ +function hasExistingMarker(markdown: string, context: string, tag: ReviewTag): boolean { + const lines = markdown.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check if this line contains the context + if (line.includes(context.slice(0, 30))) { + // Check next few lines for existing marker + for (let j = i; j < Math.min(i + 3, lines.length); j++) { + if (lines[j].includes(` + * ``` + */ +export function injectValidationMarkers( + markdown: string, + annotations: Annotation[] +): MarkerInjectionResult { + // Filter to only validation annotations + const validationAnnotations = annotations.filter( + (ann) => ann.tag && VALIDATION_TAGS.includes(ann.tag) + ); + + if (validationAnnotations.length === 0) { + return { markdown, markersAdded: 0, markers: [] }; + } + + const lines = markdown.split('\n'); + const markersToAdd: Array<{ + lineIndex: number; + tag: ReviewTag; + context: string; + }> = []; + + // For each validation annotation, find where to insert the marker + for (const ann of validationAnnotations) { + const tag = ann.tag!; + const context = ann.originalText.split('\n')[0].slice(0, 50); // First line as context + + // Skip if marker already exists + if (hasExistingMarker(markdown, context, tag)) { + continue; + } + + // Find the line containing the annotated text + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check if this line starts the annotated content + if (line.includes(ann.originalText.slice(0, 30)) || + ann.originalText.includes(line.trim())) { + + // For headings, insert marker right after the heading line + if (line.trim().startsWith('#')) { + markersToAdd.push({ lineIndex: i, tag, context: line.trim() }); + break; + } + + // For other content, find the end of the block + let endLine = i; + for (let j = i + 1; j < lines.length; j++) { + const nextLine = lines[j]; + // Stop at empty line, heading, or horizontal rule + if (!nextLine.trim() || nextLine.trim().startsWith('#') || /^-{3,}$/.test(nextLine.trim())) { + endLine = j - 1; + break; + } + endLine = j; + } + + markersToAdd.push({ lineIndex: endLine, tag, context }); + break; + } + } + } + + // Sort by line index descending (to insert from bottom up) + markersToAdd.sort((a, b) => b.lineIndex - a.lineIndex); + + // Insert markers + const resultLines = [...lines]; + const addedMarkers: Array<{ tag: ReviewTag; context: string; line: number }> = []; + + for (const marker of markersToAdd) { + const markerComment = ``; + + // Insert after the target line + resultLines.splice(marker.lineIndex + 1, 0, markerComment); + addedMarkers.push({ + tag: marker.tag, + context: marker.context, + line: marker.lineIndex + 2, // +1 for 1-based, +1 for inserted line + }); + } + + return { + markdown: resultLines.join('\n'), + markersAdded: addedMarkers.length, + markers: addedMarkers, + }; +} + +// --- Stripping --- + +/** + * Remove all validation markers from markdown. + * Useful when exporting to formats that don't need markers (Notion, HTML, etc.) + * + * @param markdown - Markdown content with markers + * @returns Clean markdown without markers + */ +export function stripValidationMarkers(markdown: string): string { + // Remove marker lines and any trailing empty line left behind + return markdown + .replace(/\n?/g, '') + .replace(/\n{3,}/g, '\n\n'); // Collapse multiple empty lines +} + +// --- Utilities --- + +/** + * Format a marker for display in the UI. + */ +export function formatMarkerForDisplay(marker: ValidationMarker): string { + const parts = [marker.tag]; + + if (marker.attributes?.by) { + parts.push(`by ${marker.attributes.by}`); + } + if (marker.attributes?.date) { + parts.push(`on ${marker.attributes.date}`); + } + + return parts.join(' '); +} + +/** + * Check if a tag is a validation tag that can be persisted. + */ +export function isValidationTag(tag: ReviewTag | undefined): tag is ReviewTag { + return tag !== undefined && VALIDATION_TAGS.includes(tag); +} + +/** + * Get the count of validation annotations in a list. + */ +export function countValidationAnnotations(annotations: Annotation[]): number { + return annotations.filter((ann) => isValidationTag(ann.tag)).length; +} diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..1fda068 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,13 @@ +{ + "name": "@plannotator/core", + "version": "0.1.0", + "description": "Core logic for Plannotator - markdown parsing, annotations, and diff export", + "main": "index.ts", + "types": "index.ts", + "exports": { + ".": "./index.ts", + "./types": "./types.ts", + "./parser": "./parser.ts" + }, + "dependencies": {} +} diff --git a/packages/core/parser.ts b/packages/core/parser.ts new file mode 100644 index 0000000..b0bc612 --- /dev/null +++ b/packages/core/parser.ts @@ -0,0 +1,336 @@ +/** + * @plannotator/core - Markdown Parser + * Parses markdown into blocks for annotation and exports feedback diffs + */ + +import { Block, Annotation, AnnotationType } from './types'; + +/** + * Parsed YAML frontmatter as key-value pairs. + */ +export interface Frontmatter { + [key: string]: string | string[]; +} + +/** + * Extract YAML frontmatter from markdown if present. + * Returns both the parsed frontmatter and the remaining markdown. + */ +export function extractFrontmatter(markdown: string): { frontmatter: Frontmatter | null; content: string } { + const trimmed = markdown.trimStart(); + if (!trimmed.startsWith('---')) { + return { frontmatter: null, content: markdown }; + } + + // Find the closing --- + const endIndex = trimmed.indexOf('\n---', 3); + if (endIndex === -1) { + return { frontmatter: null, content: markdown }; + } + + // Extract frontmatter content (between the --- delimiters) + const frontmatterRaw = trimmed.slice(4, endIndex).trim(); + const afterFrontmatter = trimmed.slice(endIndex + 4).trimStart(); + + // Parse simple YAML (key: value pairs) + const frontmatter: Frontmatter = {}; + let currentKey: string | null = null; + let currentArray: string[] | null = null; + + for (const line of frontmatterRaw.split('\n')) { + const trimmedLine = line.trim(); + + // Array item (- value) + if (trimmedLine.startsWith('- ') && currentKey) { + const value = trimmedLine.slice(2).trim(); + if (!currentArray) { + currentArray = []; + frontmatter[currentKey] = currentArray; + } + currentArray.push(value); + continue; + } + + // Key: value pair + const colonIndex = trimmedLine.indexOf(':'); + if (colonIndex > 0) { + currentKey = trimmedLine.slice(0, colonIndex).trim(); + const value = trimmedLine.slice(colonIndex + 1).trim(); + currentArray = null; + + if (value) { + frontmatter[currentKey] = value; + } + } + } + + return { frontmatter, content: afterFrontmatter }; +} + +/** + * A simplified markdown parser that splits content into linear blocks. + * For a production app, we would use a robust AST walker (remark), + * but for this demo, we want predictable text-anchoring. + */ +export const parseMarkdownToBlocks = (markdown: string): Block[] => { + const { content: cleanMarkdown } = extractFrontmatter(markdown); + const lines = cleanMarkdown.split('\n'); + const blocks: Block[] = []; + let currentId = 0; + + let buffer: string[] = []; + let currentType: Block['type'] = 'paragraph'; + let currentLevel = 0; + let bufferStartLine = 1; // Track the start line of the current buffer + + const flush = () => { + if (buffer.length > 0) { + const content = buffer.join('\n'); + blocks.push({ + id: `block-${currentId++}`, + type: currentType, + content: content, + level: currentLevel, + order: currentId, + startLine: bufferStartLine + }); + buffer = []; + } + }; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + const currentLineNum = i + 1; // 1-based index + + // Headings + if (trimmed.startsWith('#')) { + flush(); + const level = trimmed.match(/^#+/)?.[0].length || 1; + blocks.push({ + id: `block-${currentId++}`, + type: 'heading', + content: trimmed.replace(/^#+\s*/, ''), + level, + order: currentId, + startLine: currentLineNum + }); + continue; + } + + // Horizontal Rule + if (trimmed === '---' || trimmed === '***') { + flush(); + blocks.push({ + id: `block-${currentId++}`, + type: 'hr', + content: '', + order: currentId, + startLine: currentLineNum + }); + continue; + } + + // List Items (Simple detection) + if (trimmed.match(/^(\*|-|\d+\.)\s/)) { + flush(); // Treat each list item as a separate block for easier annotation + // Calculate indentation level from leading whitespace + const leadingWhitespace = line.match(/^(\s*)/)?.[1] || ''; + // Count spaces (2 spaces = 1 level) or tabs (1 tab = 1 level) + const spaceCount = leadingWhitespace.replace(/\t/g, ' ').length; + const listLevel = Math.floor(spaceCount / 2); + + // Remove list marker + let content = trimmed.replace(/^(\*|-|\d+\.)\s/, ''); + + // Check for checkbox syntax: [ ] or [x] or [X] + let checked: boolean | undefined = undefined; + const checkboxMatch = content.match(/^\[([ xX])\]\s*/); + if (checkboxMatch) { + checked = checkboxMatch[1].toLowerCase() === 'x'; + content = content.replace(/^\[([ xX])\]\s*/, ''); + } + + blocks.push({ + id: `block-${currentId++}`, + type: 'list-item', + content, + level: listLevel, + checked, + order: currentId, + startLine: currentLineNum + }); + continue; + } + + // Blockquotes + if (trimmed.startsWith('>')) { + // Check if previous was blockquote, if so, merge? No, separate for now + flush(); + blocks.push({ + id: `block-${currentId++}`, + type: 'blockquote', + content: trimmed.replace(/^>\s*/, ''), + order: currentId, + startLine: currentLineNum + }); + continue; + } + + // Code blocks (naive) + if (trimmed.startsWith('```')) { + flush(); + const codeStartLine = currentLineNum; + // Extract language from fence (e.g., ```rust → "rust") + const language = trimmed.slice(3).trim() || undefined; + // Fast forward until end of code block + let codeContent = []; + i++; // Skip start fence + while(i < lines.length && !lines[i].trim().startsWith('```')) { + codeContent.push(lines[i]); + i++; + } + blocks.push({ + id: `block-${currentId++}`, + type: 'code', + content: codeContent.join('\n'), + language, + order: currentId, + startLine: codeStartLine + }); + continue; + } + + // Tables (lines starting and containing |) + if (trimmed.startsWith('|') || (trimmed.includes('|') && trimmed.match(/^\|?.+\|.+\|?$/))) { + flush(); + const tableStartLine = currentLineNum; + const tableLines: string[] = [line]; + + // Collect all consecutive table lines + while (i + 1 < lines.length) { + const nextLine = lines[i + 1].trim(); + // Continue if line has table structure (contains | and looks like table content) + if (nextLine.startsWith('|') || (nextLine.includes('|') && nextLine.match(/^\|?.+\|.+\|?$/))) { + i++; + tableLines.push(lines[i]); + } else { + break; + } + } + + blocks.push({ + id: `block-${currentId++}`, + type: 'table', + content: tableLines.join('\n'), + order: currentId, + startLine: tableStartLine + }); + continue; + } + + // Empty lines separate paragraphs + if (trimmed === '') { + flush(); + currentType = 'paragraph'; + continue; + } + + // Accumulate paragraph text + if (buffer.length === 0) { + bufferStartLine = currentLineNum; + } + buffer.push(line); + } + + flush(); // Final flush + + return blocks; +}; + +/** + * Export annotations as a formatted diff/feedback document. + * Used to generate human-readable feedback for Claude. + */ +export const exportDiff = (blocks: Block[], annotations: Annotation[], globalAttachments: string[] = []): string => { + if (annotations.length === 0 && globalAttachments.length === 0) { + return 'No changes detected.'; + } + + // Sort annotations by block and offset + const sortedAnns = [...annotations].sort((a, b) => { + const blockA = blocks.findIndex(blk => blk.id === a.blockId); + const blockB = blocks.findIndex(blk => blk.id === b.blockId); + if (blockA !== blockB) return blockA - blockB; + return a.startOffset - b.startOffset; + }); + + let output = `# Plan Feedback\n\n`; + + // Add global reference images section if any + if (globalAttachments.length > 0) { + output += `## Reference Images\n`; + output += `Please review these reference images (use the Read tool to view):\n`; + globalAttachments.forEach((path, idx) => { + output += `${idx + 1}. \`${path}\`\n`; + }); + output += `\n`; + } + + if (annotations.length > 0) { + output += `I've reviewed this plan and have ${annotations.length} piece${annotations.length > 1 ? 's' : ''} of feedback:\n\n`; + } + + sortedAnns.forEach((ann, index) => { + const block = blocks.find(b => b.id === ann.blockId); + + // Include tag prefix if present (e.g., "## 1. @FIX Remove this") + // Include [MACRO] flag if set (indicates cross-document impact) + const tagPrefix = ann.tag ? `${ann.tag} ` : ''; + const macroFlag = ann.isMacro ? '[MACRO] ' : ''; + output += `## ${index + 1}. ${tagPrefix}${macroFlag}`; + + switch (ann.type) { + case AnnotationType.DELETION: + output += `Remove this\n`; + output += `\`\`\`\n${ann.originalText}\n\`\`\`\n`; + output += `> I don't want this in the plan.\n`; + break; + + case AnnotationType.INSERTION: + output += `Add this\n`; + output += `\`\`\`\n${ann.text}\n\`\`\`\n`; + break; + + case AnnotationType.REPLACEMENT: + output += `Change this\n`; + output += `**From:**\n\`\`\`\n${ann.originalText}\n\`\`\`\n`; + output += `**To:**\n\`\`\`\n${ann.text}\n\`\`\`\n`; + break; + + case AnnotationType.COMMENT: + output += `Feedback on: "${ann.originalText}"\n`; + output += `> ${ann.text}\n`; + break; + + case AnnotationType.GLOBAL_COMMENT: + output += `General feedback about the plan\n`; + output += `> ${ann.text}\n`; + break; + } + + // Add attached images for this annotation + if (ann.imagePaths && ann.imagePaths.length > 0) { + output += `**Attached images:**\n`; + ann.imagePaths.forEach((path: string) => { + output += `- \`${path}\`\n`; + }); + } + + output += '\n'; + }); + + output += `---\n`; + + return output; +}; diff --git a/packages/core/types.ts b/packages/core/types.ts new file mode 100644 index 0000000..89873c5 --- /dev/null +++ b/packages/core/types.ts @@ -0,0 +1,131 @@ +/** + * @plannotator/core - Type definitions + * Portable types for plan annotations and blocks + */ + +export enum AnnotationType { + DELETION = 'DELETION', + INSERTION = 'INSERTION', + REPLACEMENT = 'REPLACEMENT', + COMMENT = 'COMMENT', + GLOBAL_COMMENT = 'GLOBAL_COMMENT', +} + +/** + * Review methodology tags for structured feedback. + * Optional tags that help Claude understand the intent of annotations. + */ +export enum ReviewTag { + // Modification (action required) + TODO = '@TODO', + FIX = '@FIX', + CLARIFY = '@CLARIFY', + MISSING = '@MISSING', + ADD_EXAMPLE = '@ADD-EXAMPLE', + // Verification (fact-checking) + VERIFY = '@VERIFY', + VERIFY_SOURCES = '@VERIFY-SOURCES', + CHECK_FORMULA = '@CHECK-FORMULA', + CHECK_LINK = '@CHECK-LINK', + // Validation + OK = '@OK', + APPROVED = '@APPROVED', + LOCKED = '@LOCKED', +} + +/** + * Categories for organizing review tags in the UI dropdown. + */ +export const REVIEW_TAG_CATEGORIES = { + modification: [ + ReviewTag.TODO, + ReviewTag.FIX, + ReviewTag.CLARIFY, + ReviewTag.MISSING, + ReviewTag.ADD_EXAMPLE, + ], + verification: [ + ReviewTag.VERIFY, + ReviewTag.VERIFY_SOURCES, + ReviewTag.CHECK_FORMULA, + ReviewTag.CHECK_LINK, + ], + validation: [ReviewTag.OK, ReviewTag.APPROVED, ReviewTag.LOCKED], +} as const; + +export type EditorMode = 'selection' | 'redline'; + +export interface Annotation { + id: string; + blockId: string; // Legacy - not used with web-highlighter + startOffset: number; // Legacy + endOffset: number; // Legacy + type: AnnotationType; + tag?: ReviewTag; // Optional review methodology tag + isMacro?: boolean; // [MACRO] flag - indicates change has cross-document impact + text?: string; // For comments + originalText: string; // The text that was selected + createdAt: number; + author?: string; // Tater identity for collaborative sharing + imagePaths?: string[]; // Attached images (local paths or URLs) + // web-highlighter metadata for cross-element selections + startMeta?: { + parentTagName: string; + parentIndex: number; + textOffset: number; + }; + endMeta?: { + parentTagName: string; + parentIndex: number; + textOffset: number; + }; +} + +export interface Block { + id: string; + type: 'paragraph' | 'heading' | 'blockquote' | 'list-item' | 'code' | 'hr' | 'table'; + content: string; // Plain text content + level?: number; // For headings (1-6) or list indentation + language?: string; // For code blocks (e.g., 'rust', 'typescript') + checked?: boolean; // For checkbox list items (true = checked, false = unchecked, undefined = not a checkbox) + order: number; // Sorting order + startLine: number; // 1-based line number in source +} + +export interface DiffResult { + original: string; + modified: string; + diffText: string; +} + +// Code Review Types +export type CodeAnnotationType = 'comment' | 'suggestion' | 'concern'; + +export interface CodeAnnotation { + id: string; + type: CodeAnnotationType; + filePath: string; + lineStart: number; + lineEnd: number; + side: 'old' | 'new'; // Maps to 'deletions' | 'additions' in @pierre/diffs + text?: string; + suggestedCode?: string; + createdAt: number; + author?: string; +} + +// For @pierre/diffs integration +export interface DiffAnnotationMetadata { + annotationId: string; + type: CodeAnnotationType; + text?: string; + suggestedCode?: string; + author?: string; +} + +export interface SelectedLineRange { + start: number; + end: number; + side: 'deletions' | 'additions'; + endSide?: 'deletions' | 'additions'; +} diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 0897157..6c97565 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useMemo, useRef } from 'react'; import { parseMarkdownToBlocks, exportDiff, extractFrontmatter, Frontmatter } from '@plannotator/ui/utils/parser'; +import { isValidationTag, countValidationAnnotations, type ValidationMarker } from '@plannotator/ui/utils/markers'; import { Viewer, ViewerHandle } from '@plannotator/ui/components/Viewer'; import { AnnotationPanel } from '@plannotator/ui/components/AnnotationPanel'; import { ExportModal } from '@plannotator/ui/components/ExportModal'; @@ -327,6 +328,15 @@ const App: React.FC = () => { const [showPermissionModeSetup, setShowPermissionModeSetup] = useState(false); const [permissionMode, setPermissionMode] = useState('bypassPermissions'); const [sharingEnabled, setSharingEnabled] = useState(true); + // Plan metadata for header display + const [planTitle, setPlanTitle] = useState(null); + const [planVersion, setPlanVersion] = useState(null); + const [planTimestamp, setPlanTimestamp] = useState(null); + // Annotate mode state (for /plannotator-annotate command) + const [annotateMode, setAnnotateMode] = useState(false); + const [filePath, setFilePath] = useState(null); + const [existingMarkers, setExistingMarkers] = useState([]); + const [saveMarkersStatus, setSaveMarkersStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); const viewerRef = useRef(null); // URL-based sharing @@ -381,12 +391,32 @@ const App: React.FC = () => { if (!res.ok) throw new Error('Not in API mode'); return res.json(); }) - .then((data: { plan: string; origin?: 'claude-code' | 'opencode'; sharingEnabled?: boolean }) => { + .then((data: { + plan: string; + origin?: 'claude-code' | 'opencode'; + sharingEnabled?: boolean; + title?: string; + version?: number; + timestamp?: string; + mode?: 'plan' | 'annotate'; + filePath?: string; + existingMarkers?: ValidationMarker[]; + }) => { setMarkdown(data.plan); setIsApiMode(true); if (data.sharingEnabled !== undefined) { setSharingEnabled(data.sharingEnabled); } + // Store plan metadata + if (data.title) setPlanTitle(data.title); + if (data.version) setPlanVersion(data.version); + if (data.timestamp) setPlanTimestamp(data.timestamp); + // Detect annotate mode (for /plannotator-annotate command) + if (data.mode === 'annotate') { + setAnnotateMode(true); + if (data.filePath) setFilePath(data.filePath); + if (data.existingMarkers) setExistingMarkers(data.existingMarkers); + } if (data.origin) { setOrigin(data.origin); // For Claude Code, check if user needs to configure permission mode @@ -544,6 +574,40 @@ const App: React.FC = () => { } }; + // Save validation markers to source file (annotate mode only) + const handleSaveMarkers = async () => { + if (!annotateMode) return; + + const validationCount = countValidationAnnotations(annotations); + if (validationCount === 0) return; + + setSaveMarkersStatus('saving'); + try { + const res = await fetch('/api/save-markers', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ annotations }), + }); + const data = await res.json(); + + if (data.success) { + setSaveMarkersStatus('saved'); + // Update existing markers with newly added ones + if (data.markers && data.markers.length > 0) { + setExistingMarkers(prev => [...prev, ...data.markers]); + } + // Reset status after a delay + setTimeout(() => setSaveMarkersStatus('idle'), 2000); + } else { + setSaveMarkersStatus('error'); + setTimeout(() => setSaveMarkersStatus('idle'), 3000); + } + } catch { + setSaveMarkersStatus('error'); + setTimeout(() => setSaveMarkersStatus('idle'), 3000); + } + }; + // Global keyboard shortcuts (Cmd/Ctrl+Enter to submit) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -616,6 +680,9 @@ const App: React.FC = () => { const diffOutput = useMemo(() => exportDiff(blocks, annotations, globalAttachments), [blocks, annotations, globalAttachments]); + // Count validation annotations for "Save Markers" button + const validationAnnotationCount = useMemo(() => countValidationAnnotations(annotations), [annotations]); + const agentName = useMemo(() => { if (origin === 'opencode') return 'OpenCode'; if (origin === 'claude-code') return 'Claude Code'; @@ -655,9 +722,62 @@ const App: React.FC = () => { {agentName} )} + {annotateMode && ( + + Annotate + + )} + {filePath && ( + + {filePath.split(/[/\\]/).pop()} + + )} + {existingMarkers.length > 0 && ( + + {existingMarkers.length} marker{existingMarkers.length !== 1 ? 's' : ''} + + )} + {planTitle && planVersion && ( + + v{planVersion} + {planTimestamp && ( + + {new Date(planTimestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + )} + + )}
+ {/* Save Markers button (annotate mode only) */} + {annotateMode && validationAnnotationCount > 0 && ( + + )} + {isApiMode && ( <>
diff --git a/packages/native/__tests__/hooks.test.ts b/packages/native/__tests__/hooks.test.ts new file mode 100644 index 0000000..871c712 --- /dev/null +++ b/packages/native/__tests__/hooks.test.ts @@ -0,0 +1,101 @@ +/** + * Tests for @plannotator/native hooks + * Note: These test the hook logic without React rendering + */ + +import { describe, test, expect } from 'bun:test'; +import { createAnnotation } from '../hooks/useAnnotations'; +import { AnnotationType } from '@plannotator/core'; + +describe('createAnnotation', () => { + test('creates annotation with required fields', () => { + const annotation = createAnnotation({ + blockId: 'block-1', + type: AnnotationType.COMMENT, + originalText: 'Hello', + startOffset: 0, + endOffset: 5, + text: 'This needs work', + }); + + expect(annotation.blockId).toBe('block-1'); + expect(annotation.type).toBe(AnnotationType.COMMENT); + expect(annotation.originalText).toBe('Hello'); + expect(annotation.startOffset).toBe(0); + expect(annotation.endOffset).toBe(5); + expect(annotation.text).toBe('This needs work'); + expect(annotation.id).toBeDefined(); + expect(annotation.id).toMatch(/^ann-\d+-[a-z0-9]+$/); + expect(annotation.createdAt).toBeGreaterThan(0); + }); + + test('generates unique IDs', () => { + const ann1 = createAnnotation({ + blockId: 'b1', + type: AnnotationType.DELETION, + originalText: 'text', + startOffset: 0, + endOffset: 4, + }); + + const ann2 = createAnnotation({ + blockId: 'b1', + type: AnnotationType.DELETION, + originalText: 'text', + startOffset: 0, + endOffset: 4, + }); + + expect(ann1.id).not.toBe(ann2.id); + }); + + test('includes optional author', () => { + const annotation = createAnnotation({ + blockId: 'block-1', + type: AnnotationType.COMMENT, + originalText: 'text', + startOffset: 0, + endOffset: 4, + author: 'Claude', + }); + + expect(annotation.author).toBe('Claude'); + }); + + test('includes optional imagePaths', () => { + const annotation = createAnnotation({ + blockId: 'block-1', + type: AnnotationType.COMMENT, + originalText: 'text', + startOffset: 0, + endOffset: 4, + imagePaths: ['/path/to/image.png'], + }); + + expect(annotation.imagePaths).toEqual(['/path/to/image.png']); + }); +}); + +describe('Theme types', () => { + test('lightTheme has required properties', async () => { + const { lightTheme } = await import('../types'); + + expect(lightTheme.background).toBeDefined(); + expect(lightTheme.surface).toBeDefined(); + expect(lightTheme.text).toBeDefined(); + expect(lightTheme.primary).toBeDefined(); + expect(lightTheme.deletion).toBeDefined(); + expect(lightTheme.comment).toBeDefined(); + }); + + test('darkTheme has required properties', async () => { + const { darkTheme } = await import('../types'); + + expect(darkTheme.background).toBeDefined(); + expect(darkTheme.surface).toBeDefined(); + expect(darkTheme.text).toBeDefined(); + expect(darkTheme.primary).toBeDefined(); + expect(darkTheme.deletion).toBeDefined(); + expect(darkTheme.comment).toBeDefined(); + }); +}); diff --git a/packages/native/__tests__/integration.test.ts b/packages/native/__tests__/integration.test.ts new file mode 100644 index 0000000..291e1e0 --- /dev/null +++ b/packages/native/__tests__/integration.test.ts @@ -0,0 +1,282 @@ +/** + * Integration tests for @plannotator/native + * Tests the complete flow from markdown parsing to annotation export + */ + +import { describe, test, expect } from 'bun:test'; +import { + parseMarkdownToBlocks, + exportDiff, + AnnotationType, + type Block, + type Annotation, +} from '@plannotator/core'; +import { createAnnotation } from '../hooks/useAnnotations'; +import { lightTheme, darkTheme } from '../types'; + +describe('Full integration flow', () => { + const samplePlan = `# Implementation Plan + +## Summary +This plan adds a new authentication feature. + +## Steps + +1. Create user model +2. Add login endpoint +3. Implement JWT tokens + +## Code Example + +\`\`\`typescript +interface User { + id: string; + email: string; +} +\`\`\` + +> Note: This requires database migrations. + +--- + +Final step: Deploy to production. +`; + + test('parses markdown and creates annotations correctly', () => { + // Step 1: Parse markdown + const blocks = parseMarkdownToBlocks(samplePlan); + + // Verify blocks were created + expect(blocks.length).toBeGreaterThan(5); + + // Find specific blocks + const headingBlock = blocks.find(b => b.type === 'heading' && b.level === 1); + const listBlock = blocks.find(b => b.type === 'list-item'); + const codeBlock = blocks.find(b => b.type === 'code'); + const blockquoteBlock = blocks.find(b => b.type === 'blockquote'); + const hrBlock = blocks.find(b => b.type === 'hr'); + + expect(headingBlock).toBeDefined(); + expect(headingBlock!.content).toBe('Implementation Plan'); + + expect(listBlock).toBeDefined(); + expect(listBlock!.content).toContain('Create user model'); + + expect(codeBlock).toBeDefined(); + expect(codeBlock!.language).toBe('typescript'); + + expect(blockquoteBlock).toBeDefined(); + expect(blockquoteBlock!.content).toContain('database migrations'); + + expect(hrBlock).toBeDefined(); + }); + + test('creates annotations for parsed blocks', () => { + const blocks = parseMarkdownToBlocks(samplePlan); + const summaryBlock = blocks.find(b => b.content.includes('authentication feature')); + + expect(summaryBlock).toBeDefined(); + + // Create a comment annotation + const commentAnnotation = createAnnotation({ + blockId: summaryBlock!.id, + type: AnnotationType.COMMENT, + originalText: 'authentication feature', + startOffset: summaryBlock!.content.indexOf('authentication'), + endOffset: summaryBlock!.content.indexOf('authentication') + 'authentication feature'.length, + text: 'Should we also support OAuth?', + author: 'Reviewer', + }); + + expect(commentAnnotation.type).toBe(AnnotationType.COMMENT); + expect(commentAnnotation.text).toBe('Should we also support OAuth?'); + expect(commentAnnotation.author).toBe('Reviewer'); + expect(commentAnnotation.blockId).toBe(summaryBlock!.id); + }); + + test('creates deletion annotation', () => { + const blocks = parseMarkdownToBlocks(samplePlan); + const listBlock = blocks.find(b => b.content.includes('Deploy to production')); + + expect(listBlock).toBeDefined(); + + const deletionAnnotation = createAnnotation({ + blockId: listBlock!.id, + type: AnnotationType.DELETION, + originalText: 'Deploy to production', + startOffset: 0, + endOffset: 'Deploy to production'.length, + }); + + expect(deletionAnnotation.type).toBe(AnnotationType.DELETION); + expect(deletionAnnotation.originalText).toBe('Deploy to production'); + }); + + test('creates replacement annotation', () => { + const blocks = parseMarkdownToBlocks(samplePlan); + const listBlock = blocks.find(b => b.content.includes('JWT tokens')); + + expect(listBlock).toBeDefined(); + + const replacementAnnotation = createAnnotation({ + blockId: listBlock!.id, + type: AnnotationType.REPLACEMENT, + originalText: 'JWT tokens', + startOffset: listBlock!.content.indexOf('JWT'), + endOffset: listBlock!.content.indexOf('JWT') + 'JWT tokens'.length, + text: 'OAuth2 tokens', + }); + + expect(replacementAnnotation.type).toBe(AnnotationType.REPLACEMENT); + expect(replacementAnnotation.text).toBe('OAuth2 tokens'); + }); + + test('exports diff with all annotation types', () => { + const blocks = parseMarkdownToBlocks(samplePlan); + + // Create various annotations + const annotations: Annotation[] = []; + + // Find blocks for annotations + const summaryBlock = blocks.find(b => b.content.includes('authentication feature')); + const deployBlock = blocks.find(b => b.content.includes('Deploy to production')); + const jwtBlock = blocks.find(b => b.content.includes('JWT tokens')); + + if (summaryBlock) { + annotations.push(createAnnotation({ + blockId: summaryBlock.id, + type: AnnotationType.COMMENT, + originalText: 'authentication feature', + startOffset: 0, + endOffset: 21, + text: 'Consider OAuth support', + })); + } + + if (deployBlock) { + annotations.push(createAnnotation({ + blockId: deployBlock.id, + type: AnnotationType.DELETION, + originalText: 'Deploy to production', + startOffset: 0, + endOffset: 20, + })); + } + + if (jwtBlock) { + annotations.push(createAnnotation({ + blockId: jwtBlock.id, + type: AnnotationType.REPLACEMENT, + originalText: 'JWT', + startOffset: 0, + endOffset: 3, + text: 'OAuth2', + })); + } + + // Export diff + const diff = exportDiff(blocks, annotations); + + // Verify diff contains all annotations (exportDiff outputs human-readable format) + expect(diff).toContain('Feedback on'); + expect(diff).toContain('Consider OAuth support'); + + expect(diff).toContain('Remove this'); + expect(diff).toContain('Deploy to production'); + + expect(diff).toContain('Change this'); + expect(diff).toContain('JWT'); + expect(diff).toContain('OAuth2'); + }); + + test('global comment annotation works', () => { + const blocks = parseMarkdownToBlocks(samplePlan); + + const globalComment = createAnnotation({ + blockId: 'global', + type: AnnotationType.GLOBAL_COMMENT, + originalText: '', + startOffset: 0, + endOffset: 0, + text: 'Overall the plan looks good but needs more detail on error handling.', + }); + + expect(globalComment.type).toBe(AnnotationType.GLOBAL_COMMENT); + expect(globalComment.text).toContain('error handling'); + + // Export should include global comment (in human-readable format) + const diff = exportDiff(blocks, [globalComment]); + expect(diff).toContain('General feedback'); + expect(diff).toContain('error handling'); + }); +}); + +describe('Theme exports', () => { + test('themes have all required colors', () => { + const requiredColors = [ + 'background', + 'surface', + 'text', + 'textMuted', + 'primary', + 'border', + 'deletion', + 'comment', + 'insertion', + 'replacement', + 'success', + ]; + + for (const color of requiredColors) { + expect(lightTheme).toHaveProperty(color); + expect(darkTheme).toHaveProperty(color); + } + }); + + test('light theme has appropriate colors', () => { + expect(lightTheme.background).toBe('#ffffff'); + expect(lightTheme.text).toBe('#1f2937'); + }); + + test('dark theme has appropriate colors', () => { + expect(darkTheme.background).toBe('#1f2937'); + expect(darkTheme.text).toBe('#f9fafb'); + }); +}); + +describe('Package exports', () => { + test('core package exports work', async () => { + const core = await import('@plannotator/core'); + + // Functions + expect(typeof core.parseMarkdownToBlocks).toBe('function'); + expect(typeof core.exportDiff).toBe('function'); + expect(typeof core.extractFrontmatter).toBe('function'); + + // Enums + expect(core.AnnotationType.DELETION).toBe('DELETION'); + expect(core.AnnotationType.COMMENT).toBe('COMMENT'); + }); + + test('native hooks exports work', async () => { + // Import hooks directly (not the main index which includes RN components) + const hooks = await import('../hooks'); + + // Hooks + expect(typeof hooks.useAnnotations).toBe('function'); + expect(typeof hooks.usePlanReview).toBe('function'); + expect(typeof hooks.useTextSelection).toBe('function'); + expect(typeof hooks.createAnnotation).toBe('function'); + }); + + test('native types exports work', async () => { + // Import types directly + const types = await import('../types'); + + // Themes + expect(types.lightTheme).toBeDefined(); + expect(types.darkTheme).toBeDefined(); + expect(types.lightTheme.background).toBe('#ffffff'); + expect(types.darkTheme.background).toBe('#1f2937'); + }); +}); diff --git a/packages/native/components/AnnotationPanel.tsx b/packages/native/components/AnnotationPanel.tsx new file mode 100644 index 0000000..972c8ef --- /dev/null +++ b/packages/native/components/AnnotationPanel.tsx @@ -0,0 +1,288 @@ +/** + * AnnotationPanel - Bottom sheet showing list of annotations + * Displays all annotations with delete/edit capabilities + */ + +import React, { useMemo, useCallback } from 'react'; +import { + View, + Text, + FlatList, + Pressable, + StyleSheet, +} from 'react-native'; +import { Annotation, AnnotationType } from '@plannotator/core'; +import { PlannotatorTheme, lightTheme } from '../types'; + +interface AnnotationPanelProps { + annotations: Annotation[]; + selectedId: string | null; + theme?: PlannotatorTheme; + onSelect: (id: string) => void; + onDelete: (id: string) => void; + testID?: string; +} + +// Format timestamp to relative time +function formatTimestamp(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + + if (diff < 60000) return 'Just now'; + if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; + if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; + return new Date(timestamp).toLocaleDateString(); +} + +// Get annotation type label +function getTypeLabel(type: AnnotationType): string { + switch (type) { + case AnnotationType.DELETION: + return '🗑 Deletion'; + case AnnotationType.COMMENT: + return '💬 Comment'; + case AnnotationType.REPLACEMENT: + return '✏️ Replacement'; + case AnnotationType.INSERTION: + return '➕ Insertion'; + case AnnotationType.GLOBAL_COMMENT: + return '🌍 Global Comment'; + default: + return 'Annotation'; + } +} + +// Get type color +function getTypeColor(type: AnnotationType, theme: PlannotatorTheme): string { + switch (type) { + case AnnotationType.DELETION: + return theme.deletion; + case AnnotationType.COMMENT: + case AnnotationType.REPLACEMENT: + case AnnotationType.INSERTION: + case AnnotationType.GLOBAL_COMMENT: + return theme.comment; + default: + return theme.surface; + } +} + +export const AnnotationPanel: React.FC = ({ + annotations, + selectedId, + theme = lightTheme, + onSelect, + onDelete, + testID, +}) => { + const sortedAnnotations = useMemo( + () => [...annotations].sort((a, b) => a.createdAt - b.createdAt), + [annotations] + ); + + const styles = useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.background, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: theme.border, + }, + headerTitle: { + fontSize: 16, + fontWeight: '600', + color: theme.text, + }, + headerCount: { + fontSize: 14, + color: theme.textMuted, + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 32, + }, + emptyText: { + fontSize: 14, + color: theme.textMuted, + textAlign: 'center', + marginTop: 8, + }, + emptyIcon: { + fontSize: 32, + marginBottom: 8, + }, + annotationItem: { + padding: 12, + borderBottomWidth: 1, + borderBottomColor: theme.border, + }, + annotationItemSelected: { + backgroundColor: theme.surface, + }, + annotationHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 4, + }, + annotationType: { + flexDirection: 'row', + alignItems: 'center', + }, + typeIndicator: { + width: 8, + height: 8, + borderRadius: 4, + marginRight: 8, + }, + typeLabel: { + fontSize: 12, + fontWeight: '500', + color: theme.textMuted, + }, + timestamp: { + fontSize: 11, + color: theme.textMuted, + }, + originalText: { + fontSize: 13, + color: theme.text, + marginTop: 4, + }, + originalTextTruncated: { + maxHeight: 40, + overflow: 'hidden', + }, + commentText: { + fontSize: 13, + color: theme.primary, + fontStyle: 'italic', + marginTop: 4, + }, + actions: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: 8, + }, + deleteButton: { + paddingHorizontal: 12, + paddingVertical: 4, + borderRadius: 4, + backgroundColor: '#fef2f2', + }, + deleteButtonText: { + fontSize: 12, + color: '#dc2626', + }, + authorBadge: { + fontSize: 11, + color: theme.textMuted, + backgroundColor: theme.surface, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + marginLeft: 8, + }, + }), + [theme] + ); + + const renderAnnotation = useCallback( + ({ item }: { item: Annotation }) => { + const isSelected = item.id === selectedId; + + return ( + onSelect(item.id)} + testID={`${testID}-item-${item.id}`} + > + + + + {getTypeLabel(item.type)} + {item.author && ( + {item.author} + )} + + {formatTimestamp(item.createdAt)} + + + {/* Original text (for non-global comments) */} + {item.type !== AnnotationType.GLOBAL_COMMENT && item.originalText && ( + + "{item.originalText}" + + )} + + {/* Comment/replacement text */} + {item.text && ( + + → {item.text} + + )} + + {/* Delete action */} + + onDelete(item.id)} + testID={`${testID}-delete-${item.id}`} + > + Delete + + + + ); + }, + [styles, selectedId, theme, onSelect, onDelete, testID] + ); + + const renderEmpty = useCallback( + () => ( + + 📝 + + No annotations yet.{'\n'} + Select text to add feedback. + + + ), + [styles] + ); + + return ( + + + Annotations + + {annotations.length} item{annotations.length !== 1 ? 's' : ''} + + + + item.id} + ListEmptyComponent={renderEmpty} + /> + + ); +}; diff --git a/packages/native/components/AnnotationToolbar.tsx b/packages/native/components/AnnotationToolbar.tsx new file mode 100644 index 0000000..949e57a --- /dev/null +++ b/packages/native/components/AnnotationToolbar.tsx @@ -0,0 +1,280 @@ +/** + * AnnotationToolbar - Floating toolbar for creating annotations + * Appears above selected text with Copy, Delete, Comment actions + */ + +import React, { useState, useMemo, useCallback } from 'react'; +import { + View, + Text, + TextInput, + Pressable, + StyleSheet, + Animated, + Keyboard, +} from 'react-native'; +import { AnnotationType } from '@plannotator/core'; +import { PlannotatorTheme, lightTheme, ToolbarPosition } from '../types'; + +interface AnnotationToolbarProps { + visible: boolean; + position: ToolbarPosition; + selectedText: string; + theme?: PlannotatorTheme; + onAnnotate: (type: AnnotationType, text?: string) => void; + onCopy: () => void; + onClose: () => void; + testID?: string; +} + +type ToolbarMode = 'actions' | 'comment'; + +export const AnnotationToolbar: React.FC = ({ + visible, + position, + selectedText, + theme = lightTheme, + onAnnotate, + onCopy, + onClose, + testID, +}) => { + const [mode, setMode] = useState('actions'); + const [commentText, setCommentText] = useState(''); + const [fadeAnim] = useState(new Animated.Value(0)); + + // Animate in/out + React.useEffect(() => { + Animated.timing(fadeAnim, { + toValue: visible ? 1 : 0, + duration: 150, + useNativeDriver: true, + }).start(); + + if (!visible) { + setMode('actions'); + setCommentText(''); + } + }, [visible, fadeAnim]); + + const handleCopy = useCallback(() => { + onCopy(); + onClose(); + }, [onCopy, onClose]); + + const handleDelete = useCallback(() => { + onAnnotate(AnnotationType.DELETION); + onClose(); + }, [onAnnotate, onClose]); + + const handleComment = useCallback(() => { + setMode('comment'); + }, []); + + const handleSubmitComment = useCallback(() => { + if (commentText.trim()) { + onAnnotate(AnnotationType.COMMENT, commentText.trim()); + setCommentText(''); + Keyboard.dismiss(); + onClose(); + } + }, [commentText, onAnnotate, onClose]); + + const handleCancelComment = useCallback(() => { + setMode('actions'); + setCommentText(''); + Keyboard.dismiss(); + }, []); + + const styles = useMemo( + () => + StyleSheet.create({ + container: { + position: 'absolute', + left: position.x, + top: position.y - 50, // Position above selection + minWidth: 160, + backgroundColor: theme.surface, + borderRadius: 8, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + borderWidth: 1, + borderColor: theme.border, + }, + actionsRow: { + flexDirection: 'row', + alignItems: 'center', + padding: 4, + }, + button: { + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 4, + }, + buttonPressed: { + backgroundColor: theme.border, + }, + buttonText: { + fontSize: 14, + color: theme.text, + }, + deleteButton: { + // Red tint for delete + }, + deleteButtonText: { + color: '#dc2626', + }, + separator: { + width: 1, + height: 20, + backgroundColor: theme.border, + marginHorizontal: 4, + }, + closeButton: { + padding: 8, + }, + closeButtonText: { + fontSize: 16, + color: theme.textMuted, + }, + // Comment input mode + commentContainer: { + padding: 8, + minWidth: 240, + }, + commentInput: { + backgroundColor: theme.background, + borderRadius: 6, + borderWidth: 1, + borderColor: theme.border, + padding: 8, + fontSize: 14, + color: theme.text, + minHeight: 60, + maxHeight: 120, + textAlignVertical: 'top', + }, + commentActions: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: 8, + gap: 8, + }, + cancelButton: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 4, + }, + cancelButtonText: { + color: theme.textMuted, + fontSize: 14, + }, + submitButton: { + backgroundColor: theme.primary, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 4, + }, + submitButtonDisabled: { + opacity: 0.5, + }, + submitButtonText: { + color: '#ffffff', + fontSize: 14, + fontWeight: '500', + }, + }), + [theme, position] + ); + + if (!visible) return null; + + return ( + + {mode === 'actions' ? ( + + {/* Copy button */} + [styles.button, pressed && styles.buttonPressed]} + onPress={handleCopy} + testID={`${testID}-copy`} + > + 📋 Copy + + + + + {/* Delete button */} + [ + styles.button, + styles.deleteButton, + pressed && styles.buttonPressed, + ]} + onPress={handleDelete} + testID={`${testID}-delete`} + > + 🗑 Delete + + + + + {/* Comment button */} + [styles.button, pressed && styles.buttonPressed]} + onPress={handleComment} + testID={`${testID}-comment`} + > + 💬 Comment + + + + + {/* Close button */} + + + + + ) : ( + + + + + Cancel + + + Add + + + + )} + + ); +}; diff --git a/packages/native/components/BlockRenderer.tsx b/packages/native/components/BlockRenderer.tsx new file mode 100644 index 0000000..cdc14f7 --- /dev/null +++ b/packages/native/components/BlockRenderer.tsx @@ -0,0 +1,318 @@ +/** + * BlockRenderer - Renders a single markdown block + * Handles all block types: heading, paragraph, code, list-item, blockquote, hr, table + */ + +import React, { useMemo, useCallback } from 'react'; +import { View, Text, StyleSheet, Pressable, ViewStyle } from 'react-native'; +import { Block, Annotation, AnnotationType } from '@plannotator/core'; +import { InlineMarkdown } from './InlineMarkdown'; +import { CodeBlock } from './CodeBlock'; +import { PlannotatorTheme, lightTheme, SelectionRange } from '../types'; + +interface BlockRendererProps { + block: Block; + annotations: Annotation[]; + theme?: PlannotatorTheme; + onTextSelect?: (range: SelectionRange) => void; + onCopy?: (content: string) => void; + testID?: string; +} + +// Helper to get highlight style for annotation type +function getHighlightStyle(type: AnnotationType, theme: PlannotatorTheme): ViewStyle { + switch (type) { + case AnnotationType.DELETION: + return { backgroundColor: theme.deletion }; + case AnnotationType.COMMENT: + case AnnotationType.REPLACEMENT: + case AnnotationType.INSERTION: + return { backgroundColor: theme.comment }; + default: + return {}; + } +} + +// Helper to parse table content +function parseTable(content: string): { headers: string[]; rows: string[][] } { + const lines = content.split('\n').filter(line => line.trim()); + if (lines.length === 0) return { headers: [], rows: [] }; + + const parseRow = (line: string): string[] => + line + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map(cell => cell.trim()); + + const headers = parseRow(lines[0]); + const rows: string[][] = []; + + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (/^[\|\-:\s]+$/.test(line)) continue; // Skip separator + rows.push(parseRow(line)); + } + + return { headers, rows }; +} + +export const BlockRenderer: React.FC = ({ + block, + annotations, + theme = lightTheme, + onTextSelect, + onCopy, + testID, +}) => { + const styles = useMemo( + () => + StyleSheet.create({ + // Headings + h1: { + fontSize: 24, + fontWeight: 'bold', + marginTop: 24, + marginBottom: 16, + color: theme.text, + }, + h2: { + fontSize: 20, + fontWeight: '600', + marginTop: 20, + marginBottom: 12, + color: theme.text, + }, + h3: { + fontSize: 18, + fontWeight: '600', + marginTop: 16, + marginBottom: 8, + color: theme.text, + }, + // Paragraph + paragraph: { + marginVertical: 8, + }, + // List item + listItemContainer: { + flexDirection: 'row', + marginVertical: 4, + }, + listItemBullet: { + width: 20, + marginRight: 8, + color: theme.primary, + }, + listItemContent: { + flex: 1, + }, + checkbox: { + marginRight: 8, + }, + checkedText: { + textDecorationLine: 'line-through', + color: theme.textMuted, + }, + // Blockquote + blockquote: { + borderLeftWidth: 3, + borderLeftColor: theme.primary, + paddingLeft: 12, + marginVertical: 8, + }, + blockquoteText: { + fontStyle: 'italic', + color: theme.textMuted, + }, + // Horizontal rule + hr: { + height: 1, + backgroundColor: theme.border, + marginVertical: 24, + }, + // Table + table: { + marginVertical: 12, + borderWidth: 1, + borderColor: theme.border, + borderRadius: 8, + overflow: 'hidden', + }, + tableRow: { + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: theme.border, + }, + tableHeader: { + flex: 1, + padding: 8, + backgroundColor: theme.surface, + }, + tableHeaderText: { + fontWeight: '600', + color: theme.text, + }, + tableCell: { + flex: 1, + padding: 8, + }, + tableCellText: { + color: theme.text, + }, + // Highlighted text + highlighted: { + borderRadius: 2, + }, + }), + [theme] + ); + + // Render text with highlights for annotations + const renderHighlightedText = useCallback( + (content: string, blockAnnotations: Annotation[]) => { + if (blockAnnotations.length === 0) { + return ; + } + + // Sort annotations by start offset + const sorted = [...blockAnnotations].sort((a, b) => a.startOffset - b.startOffset); + + const parts: React.ReactNode[] = []; + let lastIndex = 0; + + sorted.forEach((ann, idx) => { + // Text before annotation + if (ann.startOffset > lastIndex) { + parts.push( + + ); + } + + // Highlighted text + parts.push( + + {content.slice(ann.startOffset, ann.endOffset)} + + ); + + lastIndex = ann.endOffset; + }); + + // Remaining text + if (lastIndex < content.length) { + parts.push( + + ); + } + + return {parts}; + }, + [theme, styles.highlighted] + ); + + // Block-specific renderers + switch (block.type) { + case 'heading': { + const headingStyle = + block.level === 1 ? styles.h1 : block.level === 2 ? styles.h2 : styles.h3; + + return ( + + + {renderHighlightedText(block.content, annotations)} + + + ); + } + + case 'code': + return ; + + case 'list-item': { + const indent = (block.level || 0) * 16; + const isCheckbox = block.checked !== undefined; + const bullet = isCheckbox + ? block.checked + ? '☑' + : '☐' + : block.level === 0 + ? '•' + : block.level === 1 + ? '◦' + : '▪'; + + return ( + + {bullet} + + + {renderHighlightedText(block.content, annotations)} + + + + ); + } + + case 'blockquote': + return ( + + + {renderHighlightedText(block.content, annotations)} + + + ); + + case 'hr': + return ; + + case 'table': { + const { headers, rows } = parseTable(block.content); + + return ( + + {/* Header row */} + + {headers.map((header, i) => ( + + + + ))} + + + {/* Data rows */} + {rows.map((row, rowIdx) => ( + + {row.map((cell, cellIdx) => ( + + + + ))} + + ))} + + ); + } + + default: + // Paragraph + return ( + + {renderHighlightedText(block.content, annotations)} + + ); + } +}; diff --git a/packages/native/components/CodeBlock.tsx b/packages/native/components/CodeBlock.tsx new file mode 100644 index 0000000..407602a --- /dev/null +++ b/packages/native/components/CodeBlock.tsx @@ -0,0 +1,120 @@ +/** + * CodeBlock - Renders code blocks with syntax highlighting + * Uses react-native-syntax-highlighter for highlighting + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { View, Text, Pressable, StyleSheet } from 'react-native'; +import { Block } from '@plannotator/core'; +import { PlannotatorTheme, lightTheme } from '../types'; + +// Note: In actual Happy integration, use: +// import SyntaxHighlighter from 'react-native-syntax-highlighter'; +// import { atomOneDark } from 'react-syntax-highlighter/styles/hljs'; + +interface CodeBlockProps { + block: Block; + theme?: PlannotatorTheme; + onCopy?: (content: string) => void; + testID?: string; +} + +export const CodeBlock: React.FC = ({ + block, + theme = lightTheme, + onCopy, + testID, +}) => { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(async () => { + try { + // Note: In Happy, use Clipboard.setStringAsync from expo-clipboard + onCopy?.(block.content); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy:', err); + } + }, [block.content, onCopy]); + + const styles = useMemo( + () => + StyleSheet.create({ + container: { + marginVertical: 12, + borderRadius: 8, + backgroundColor: theme.surface, + borderWidth: 1, + borderColor: theme.border, + overflow: 'hidden', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 8, + backgroundColor: theme.border, + }, + language: { + fontSize: 12, + color: theme.textMuted, + fontFamily: 'monospace', + }, + copyButton: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + backgroundColor: theme.surface, + }, + copyButtonText: { + fontSize: 12, + color: copied ? theme.success : theme.textMuted, + }, + codeContainer: { + padding: 12, + }, + code: { + fontFamily: 'monospace', + fontSize: 13, + lineHeight: 20, + color: theme.text, + }, + }), + [theme, copied] + ); + + return ( + + {/* Header with language and copy button */} + + + {block.language || 'code'} + + + + {copied ? '✓ Copied' : 'Copy'} + + + + + {/* Code content */} + + {/* + In actual Happy integration, replace with: + + {block.content} + + */} + + {block.content} + + + + ); +}; diff --git a/packages/native/components/InlineMarkdown.tsx b/packages/native/components/InlineMarkdown.tsx new file mode 100644 index 0000000..d9d965c --- /dev/null +++ b/packages/native/components/InlineMarkdown.tsx @@ -0,0 +1,153 @@ +/** + * InlineMarkdown - Renders inline markdown formatting + * Handles: **bold**, *italic*, `code`, [links](url) + */ + +import React, { useMemo } from 'react'; +import { Text, StyleSheet, Linking, TextStyle } from 'react-native'; +import { PlannotatorTheme, lightTheme } from '../types'; + +interface InlineMarkdownProps { + text: string; + theme?: PlannotatorTheme; + style?: TextStyle; +} + +interface TextPart { + type: 'text' | 'bold' | 'italic' | 'code' | 'link'; + content: string; + url?: string; +} + +function parseInlineMarkdown(text: string): TextPart[] { + const parts: TextPart[] = []; + let remaining = text; + + while (remaining.length > 0) { + // Bold: **text** + let match = remaining.match(/^\*\*(.+?)\*\*/); + if (match) { + parts.push({ type: 'bold', content: match[1] }); + remaining = remaining.slice(match[0].length); + continue; + } + + // Italic: *text* + match = remaining.match(/^\*(.+?)\*/); + if (match) { + parts.push({ type: 'italic', content: match[1] }); + remaining = remaining.slice(match[0].length); + continue; + } + + // Inline code: `code` + match = remaining.match(/^`([^`]+)`/); + if (match) { + parts.push({ type: 'code', content: match[1] }); + remaining = remaining.slice(match[0].length); + continue; + } + + // Links: [text](url) + match = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/); + if (match) { + parts.push({ type: 'link', content: match[1], url: match[2] }); + remaining = remaining.slice(match[0].length); + continue; + } + + // Find next special character or consume one regular character + const nextSpecial = remaining.slice(1).search(/[\*`\[]/); + if (nextSpecial === -1) { + parts.push({ type: 'text', content: remaining }); + break; + } else { + parts.push({ type: 'text', content: remaining.slice(0, nextSpecial + 1) }); + remaining = remaining.slice(nextSpecial + 1); + } + } + + return parts; +} + +export const InlineMarkdown: React.FC = ({ + text, + theme = lightTheme, + style, +}) => { + const parts = useMemo(() => parseInlineMarkdown(text), [text]); + + const styles = useMemo( + () => + StyleSheet.create({ + text: { + color: theme.text, + fontSize: 15, + lineHeight: 22, + ...style, + }, + bold: { + fontWeight: '600', + }, + italic: { + fontStyle: 'italic', + }, + code: { + fontFamily: 'monospace', + fontSize: 13, + backgroundColor: theme.surface, + paddingHorizontal: 4, + paddingVertical: 2, + borderRadius: 4, + }, + link: { + color: theme.primary, + textDecorationLine: 'underline', + }, + }), + [theme, style] + ); + + const handleLinkPress = (url: string) => { + Linking.openURL(url).catch(err => console.error('Failed to open URL:', err)); + }; + + return ( + + {parts.map((part, index) => { + switch (part.type) { + case 'bold': + return ( + + {part.content} + + ); + case 'italic': + return ( + + {part.content} + + ); + case 'code': + return ( + + {part.content} + + ); + case 'link': + return ( + handleLinkPress(part.url!)} + > + {part.content} + + ); + default: + return {part.content}; + } + })} + + ); +}; diff --git a/packages/native/components/PlanViewer.tsx b/packages/native/components/PlanViewer.tsx new file mode 100644 index 0000000..5fcd6f7 --- /dev/null +++ b/packages/native/components/PlanViewer.tsx @@ -0,0 +1,230 @@ +/** + * PlanViewer - Main component for viewing and annotating a plan + * Renders markdown blocks with annotation support + */ + +import React, { useMemo, useCallback } from 'react'; +import { + ScrollView, + View, + Text, + Pressable, + StyleSheet, + Platform, +} from 'react-native'; +import { Block, Annotation, AnnotationType, EditorMode } from '@plannotator/core'; +import { BlockRenderer } from './BlockRenderer'; +import { AnnotationToolbar } from './AnnotationToolbar'; +import { PlannotatorTheme, lightTheme, SelectionRange, ToolbarPosition } from '../types'; +import { useTextSelection } from '../hooks/useTextSelection'; + +interface PlanViewerProps { + blocks: Block[]; + annotations: Annotation[]; + mode: EditorMode; + theme?: PlannotatorTheme; + onAddAnnotation: (annotation: Annotation) => void; + onSelectAnnotation?: (id: string) => void; + onCopy?: (content: string) => void; + testID?: string; +} + +export const PlanViewer: React.FC = ({ + blocks, + annotations, + mode, + theme = lightTheme, + onAddAnnotation, + onSelectAnnotation, + onCopy, + testID, +}) => { + const { + selection, + toolbarPosition, + isSelecting, + handleLongPress, + clearSelection, + } = useTextSelection(); + + const styles = useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.background, + }, + scrollView: { + flex: 1, + }, + contentContainer: { + padding: 16, + paddingBottom: 100, // Space for bottom sheet + }, + // Header buttons + headerButtons: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginBottom: 8, + gap: 8, + }, + headerButton: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, + backgroundColor: theme.surface, + borderWidth: 1, + borderColor: theme.border, + }, + headerButtonText: { + fontSize: 12, + color: theme.textMuted, + marginLeft: 4, + }, + // Mode indicator + modeIndicator: { + position: 'absolute', + bottom: 16, + left: 16, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + backgroundColor: mode === 'redline' ? theme.deletion : theme.comment, + }, + modeIndicatorText: { + fontSize: 12, + fontWeight: '500', + color: theme.text, + }, + // Block wrapper for touch handling + blockWrapper: { + // Needed for proper touch handling + }, + }), + [theme, mode] + ); + + // Get annotations for a specific block + const getBlockAnnotations = useCallback( + (blockId: string) => annotations.filter(a => a.blockId === blockId), + [annotations] + ); + + // Handle creating annotation from selection + const handleAnnotate = useCallback( + (type: AnnotationType, text?: string) => { + if (!selection) return; + + const newAnnotation: Annotation = { + id: `ann-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + blockId: selection.blockId, + startOffset: selection.start, + endOffset: selection.end, + type, + text, + originalText: selection.text, + createdAt: Date.now(), + }; + + onAddAnnotation(newAnnotation); + clearSelection(); + }, + [selection, onAddAnnotation, clearSelection] + ); + + // Handle copy from toolbar + const handleToolbarCopy = useCallback(() => { + if (selection && onCopy) { + onCopy(selection.text); + } + clearSelection(); + }, [selection, onCopy, clearSelection]); + + // Copy entire plan + const handleCopyPlan = useCallback(() => { + const fullText = blocks.map(b => b.content).join('\n\n'); + onCopy?.(fullText); + }, [blocks, onCopy]); + + // Long press on a block + const handleBlockLongPress = useCallback( + (block: Block, event: any) => { + if (mode === 'redline') { + // In redline mode, auto-delete on selection + const newAnnotation: Annotation = { + id: `ann-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + blockId: block.id, + startOffset: 0, + endOffset: block.content.length, + type: AnnotationType.DELETION, + originalText: block.content, + createdAt: Date.now(), + }; + onAddAnnotation(newAnnotation); + } else { + // In selection mode, show toolbar + handleLongPress(block, event); + } + }, + [mode, handleLongPress, onAddAnnotation] + ); + + return ( + + + {/* Header buttons */} + + + 📋 + Copy plan + + + + {/* Render blocks */} + {blocks.map((block, index) => ( + handleBlockLongPress(block, event)} + delayLongPress={300} + > + + + ))} + + + {/* Annotation Toolbar (floating) */} + {toolbarPosition && selection && ( + + )} + + {/* Mode indicator */} + + + {mode === 'redline' ? '🔴 Redline Mode' : '✏️ Selection Mode'} + + + + ); +}; diff --git a/packages/native/components/PlannotatorModal.tsx b/packages/native/components/PlannotatorModal.tsx new file mode 100644 index 0000000..ebd7496 --- /dev/null +++ b/packages/native/components/PlannotatorModal.tsx @@ -0,0 +1,372 @@ +/** + * PlannotatorModal - Full-screen modal for plan review + * Combines PlanViewer, AnnotationPanel, and action buttons + */ + +import React, { useMemo, useCallback, useState } from 'react'; +import { + Modal, + View, + Text, + Pressable, + SafeAreaView, + StyleSheet, + useColorScheme, + ActivityIndicator, + Dimensions, +} from 'react-native'; +import { parseMarkdownToBlocks, exportDiff, EditorMode } from '@plannotator/core'; +import { PlanViewer } from './PlanViewer'; +import { AnnotationPanel } from './AnnotationPanel'; +import { useAnnotations } from '../hooks/useAnnotations'; +import { usePlanReview } from '../hooks/usePlanReview'; +import { PlannotatorTheme, lightTheme, darkTheme } from '../types'; + +interface PlannotatorModalProps { + visible: boolean; + onClose: () => void; + planMarkdown: string; + sessionId?: string; + apiEndpoint?: string; + onApprove?: () => void; + onSendFeedback?: (feedback: string) => void; + testID?: string; +} + +const { height: SCREEN_HEIGHT } = Dimensions.get('window'); +const PANEL_HEIGHT = SCREEN_HEIGHT * 0.35; + +export const PlannotatorModal: React.FC = ({ + visible, + onClose, + planMarkdown, + sessionId, + apiEndpoint, + onApprove, + onSendFeedback, + testID, +}) => { + // Theme based on system preference + const colorScheme = useColorScheme(); + const theme: PlannotatorTheme = colorScheme === 'dark' ? darkTheme : lightTheme; + + // State + const [mode, setMode] = useState('selection'); + const [showPanel, setShowPanel] = useState(true); + const [selectedAnnotationId, setSelectedAnnotationId] = useState(null); + + // Parse markdown into blocks + const blocks = useMemo( + () => parseMarkdownToBlocks(planMarkdown), + [planMarkdown] + ); + + // Annotations state + const { + annotations, + addAnnotation, + removeAnnotation, + annotationCount, + } = useAnnotations(); + + // Review actions + const { isSubmitting, approve, sendFeedback } = usePlanReview({ + sessionId, + apiEndpoint, + }); + + // Handle approve + const handleApprove = useCallback(async () => { + await approve(); + onApprove?.(); + onClose(); + }, [approve, onApprove, onClose]); + + // Handle send feedback + const handleSendFeedback = useCallback(async () => { + const result = await sendFeedback(blocks, annotations); + onSendFeedback?.(result.feedback || ''); + onClose(); + }, [sendFeedback, blocks, annotations, onSendFeedback, onClose]); + + // Copy to clipboard (placeholder - needs expo-clipboard in Happy) + const handleCopy = useCallback((content: string) => { + console.log('Copy:', content.slice(0, 100) + '...'); + // In Happy: Clipboard.setStringAsync(content); + }, []); + + // Toggle mode + const toggleMode = useCallback(() => { + setMode(m => (m === 'selection' ? 'redline' : 'selection')); + }, []); + + const styles = useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.background, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: theme.border, + backgroundColor: theme.surface, + }, + headerLeft: { + flexDirection: 'row', + alignItems: 'center', + }, + closeButton: { + padding: 4, + }, + closeButtonText: { + fontSize: 16, + color: theme.primary, + }, + title: { + fontSize: 16, + fontWeight: '600', + color: theme.text, + marginLeft: 12, + }, + headerRight: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + modeButton: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, + backgroundColor: theme.background, + borderWidth: 1, + borderColor: theme.border, + }, + modeButtonActive: { + backgroundColor: theme.deletion, + borderColor: theme.deletion, + }, + modeButtonText: { + fontSize: 12, + color: theme.textMuted, + }, + modeButtonTextActive: { + color: '#dc2626', + fontWeight: '500', + }, + panelButton: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, + backgroundColor: theme.background, + borderWidth: 1, + borderColor: theme.border, + flexDirection: 'row', + alignItems: 'center', + }, + panelButtonText: { + fontSize: 12, + color: theme.textMuted, + }, + badge: { + backgroundColor: theme.primary, + borderRadius: 10, + paddingHorizontal: 6, + paddingVertical: 2, + marginLeft: 6, + }, + badgeText: { + fontSize: 10, + color: '#ffffff', + fontWeight: '600', + }, + content: { + flex: 1, + }, + viewer: { + flex: 1, + }, + panel: { + height: PANEL_HEIGHT, + borderTopWidth: 1, + borderTopColor: theme.border, + }, + footer: { + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + borderTopWidth: 1, + borderTopColor: theme.border, + backgroundColor: theme.surface, + gap: 12, + }, + feedbackButton: { + paddingHorizontal: 16, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: theme.comment, + flexDirection: 'row', + alignItems: 'center', + }, + feedbackButtonDisabled: { + opacity: 0.5, + }, + feedbackButtonText: { + fontSize: 14, + fontWeight: '500', + color: '#92400e', + }, + approveButton: { + paddingHorizontal: 16, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: theme.success, + flexDirection: 'row', + alignItems: 'center', + }, + approveButtonText: { + fontSize: 14, + fontWeight: '500', + color: '#ffffff', + }, + loadingIndicator: { + marginRight: 8, + }, + }), + [theme] + ); + + return ( + + + {/* Header */} + + + + Cancel + + Review Plan + + + + {/* Mode toggle */} + + + {mode === 'redline' ? '🔴 Redline' : '✏️ Select'} + + + + {/* Panel toggle */} + setShowPanel(p => !p)} + > + + {showPanel ? '⬇️ Panel' : '⬆️ Panel'} + + {annotationCount > 0 && ( + + {annotationCount} + + )} + + + + + {/* Content area */} + + {/* Plan viewer */} + + + + + {/* Annotation panel */} + {showPanel && ( + + + + )} + + + {/* Footer with action buttons */} + + {/* Send Feedback button */} + + {isSubmitting && ( + + )} + + Send Feedback ({annotationCount}) + + + + {/* Approve button */} + + {isSubmitting && ( + + )} + Approve ✓ + + + + + ); +}; diff --git a/packages/native/hooks/index.ts b/packages/native/hooks/index.ts new file mode 100644 index 0000000..dd58403 --- /dev/null +++ b/packages/native/hooks/index.ts @@ -0,0 +1,12 @@ +/** + * @plannotator/native hooks + */ + +export { useAnnotations, createAnnotation } from './useAnnotations'; +export type { UseAnnotationsReturn } from './useAnnotations'; + +export { usePlanReview } from './usePlanReview'; +export type { UsePlanReviewReturn, ReviewResult, PlanReviewOptions } from './usePlanReview'; + +export { useTextSelection } from './useTextSelection'; +export type { UseTextSelectionReturn } from './useTextSelection'; diff --git a/packages/native/hooks/useAnnotations.ts b/packages/native/hooks/useAnnotations.ts new file mode 100644 index 0000000..3a54ac2 --- /dev/null +++ b/packages/native/hooks/useAnnotations.ts @@ -0,0 +1,86 @@ +/** + * useAnnotations - State management for annotations + * Can be used with Zustand in Happy or standalone with useState + */ + +import { useState, useCallback, useMemo } from 'react'; +import { Annotation, AnnotationType } from '@plannotator/core'; + +export interface UseAnnotationsReturn { + annotations: Annotation[]; + addAnnotation: (annotation: Annotation) => void; + removeAnnotation: (id: string) => void; + updateAnnotation: (id: string, updates: Partial) => void; + clearAnnotations: () => void; + getAnnotationsByBlock: (blockId: string) => Annotation[]; + annotationCount: number; +} + +/** + * Hook for managing annotation state + */ +export function useAnnotations(initialAnnotations: Annotation[] = []): UseAnnotationsReturn { + const [annotations, setAnnotations] = useState(initialAnnotations); + + const addAnnotation = useCallback((annotation: Annotation) => { + setAnnotations(prev => [...prev, annotation]); + }, []); + + const removeAnnotation = useCallback((id: string) => { + setAnnotations(prev => prev.filter(a => a.id !== id)); + }, []); + + const updateAnnotation = useCallback((id: string, updates: Partial) => { + setAnnotations(prev => + prev.map(a => (a.id === id ? { ...a, ...updates } : a)) + ); + }, []); + + const clearAnnotations = useCallback(() => { + setAnnotations([]); + }, []); + + const getAnnotationsByBlock = useCallback( + (blockId: string) => annotations.filter(a => a.blockId === blockId), + [annotations] + ); + + const annotationCount = useMemo(() => annotations.length, [annotations]); + + return { + annotations, + addAnnotation, + removeAnnotation, + updateAnnotation, + clearAnnotations, + getAnnotationsByBlock, + annotationCount, + }; +} + +/** + * Helper to create a new annotation + */ +export function createAnnotation(params: { + blockId: string; + type: AnnotationType; + originalText: string; + startOffset: number; + endOffset: number; + text?: string; + author?: string; + imagePaths?: string[]; +}): Annotation { + return { + id: `ann-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + blockId: params.blockId, + type: params.type, + originalText: params.originalText, + startOffset: params.startOffset, + endOffset: params.endOffset, + text: params.text, + author: params.author, + imagePaths: params.imagePaths, + createdAt: Date.now(), + }; +} diff --git a/packages/native/hooks/usePlanReview.ts b/packages/native/hooks/usePlanReview.ts new file mode 100644 index 0000000..7ea8d81 --- /dev/null +++ b/packages/native/hooks/usePlanReview.ts @@ -0,0 +1,150 @@ +/** + * usePlanReview - Hook for plan review actions (approve/deny/feedback) + */ + +import { useState, useCallback } from 'react'; +import { Block, Annotation, exportDiff } from '@plannotator/core'; + +export type ReviewAction = 'approve' | 'deny' | 'feedback'; + +export interface ReviewResult { + action: ReviewAction; + feedback?: string; + timestamp: number; +} + +export interface UsePlanReviewReturn { + isSubmitting: boolean; + lastResult: ReviewResult | null; + approve: () => Promise; + deny: (reason?: string) => Promise; + sendFeedback: (blocks: Block[], annotations: Annotation[], globalAttachments?: string[]) => Promise; + reset: () => void; +} + +export interface PlanReviewOptions { + /** + * Called when an action is performed + */ + onAction?: (result: ReviewResult) => void; + + /** + * API endpoint for submitting reviews (optional) + * If not provided, actions are handled locally + */ + apiEndpoint?: string; + + /** + * Session ID for the current review + */ + sessionId?: string; +} + +/** + * Hook for handling plan review actions + */ +export function usePlanReview(options: PlanReviewOptions = {}): UsePlanReviewReturn { + const { onAction, apiEndpoint, sessionId } = options; + const [isSubmitting, setIsSubmitting] = useState(false); + const [lastResult, setLastResult] = useState(null); + + const handleResult = useCallback( + (result: ReviewResult) => { + setLastResult(result); + onAction?.(result); + return result; + }, + [onAction] + ); + + const submitToApi = useCallback( + async (action: ReviewAction, feedback?: string): Promise => { + if (!apiEndpoint) return; + + const endpoint = action === 'approve' ? '/api/approve' : '/api/deny'; + + try { + await fetch(`${apiEndpoint}${endpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId, + feedback, + }), + }); + } catch (error) { + console.error('Failed to submit to API:', error); + throw error; + } + }, + [apiEndpoint, sessionId] + ); + + const approve = useCallback(async (): Promise => { + setIsSubmitting(true); + try { + await submitToApi('approve'); + const result: ReviewResult = { + action: 'approve', + timestamp: Date.now(), + }; + return handleResult(result); + } finally { + setIsSubmitting(false); + } + }, [submitToApi, handleResult]); + + const deny = useCallback( + async (reason?: string): Promise => { + setIsSubmitting(true); + try { + await submitToApi('deny', reason); + const result: ReviewResult = { + action: 'deny', + feedback: reason, + timestamp: Date.now(), + }; + return handleResult(result); + } finally { + setIsSubmitting(false); + } + }, + [submitToApi, handleResult] + ); + + const sendFeedback = useCallback( + async ( + blocks: Block[], + annotations: Annotation[], + globalAttachments?: string[] + ): Promise => { + setIsSubmitting(true); + try { + const feedback = exportDiff(blocks, annotations, globalAttachments); + await submitToApi('feedback', feedback); + const result: ReviewResult = { + action: 'feedback', + feedback, + timestamp: Date.now(), + }; + return handleResult(result); + } finally { + setIsSubmitting(false); + } + }, + [submitToApi, handleResult] + ); + + const reset = useCallback(() => { + setLastResult(null); + }, []); + + return { + isSubmitting, + lastResult, + approve, + deny, + sendFeedback, + reset, + }; +} diff --git a/packages/native/hooks/useTextSelection.ts b/packages/native/hooks/useTextSelection.ts new file mode 100644 index 0000000..5eabd41 --- /dev/null +++ b/packages/native/hooks/useTextSelection.ts @@ -0,0 +1,163 @@ +/** + * useTextSelection - Hook for handling text selection on mobile + * + * IMPORTANT: React Native doesn't have a native text selection API like web. + * This hook provides a simplified approach using long-press gestures. + * + * For proper text highlighting, integrate with: + * - rn-text-touch-highlight (recommended) + * - react-native-highlighter + * + * Current approach: Long-press on a block selects the entire block. + */ + +import { useState, useCallback, useRef } from 'react'; +import { GestureResponderEvent, NativeSyntheticEvent, TextLayoutEventData } from 'react-native'; +import { Block } from '@plannotator/core'; +import { SelectionRange, ToolbarPosition } from '../types'; + +export interface UseTextSelectionReturn { + // Current selection state + selection: SelectionRange | null; + toolbarPosition: ToolbarPosition | null; + isSelecting: boolean; + + // Handlers + handleLongPress: (block: Block, event: GestureResponderEvent) => void; + handleTextLayout: (block: Block, event: NativeSyntheticEvent) => void; + clearSelection: () => void; + + // For rn-text-touch-highlight integration + handleHighlight: (range: { start: number; end: number; text: string }, blockId: string) => void; +} + +interface TextLayoutCache { + [blockId: string]: { + lines: Array<{ x: number; y: number; width: number; height: number; text: string }>; + pageX: number; + pageY: number; + }; +} + +export function useTextSelection(): UseTextSelectionReturn { + const [selection, setSelection] = useState(null); + const [toolbarPosition, setToolbarPosition] = useState(null); + const [isSelecting, setIsSelecting] = useState(false); + + // Cache text layouts for position calculation + const textLayoutCache = useRef({}); + + /** + * Handle long-press on a block (simplified: selects entire block) + */ + const handleLongPress = useCallback( + (block: Block, event: GestureResponderEvent) => { + const { pageX, pageY } = event.nativeEvent; + + setSelection({ + start: 0, + end: block.content.length, + text: block.content, + blockId: block.id, + }); + + // Position toolbar above the press point + setToolbarPosition({ + x: Math.max(0, pageX - 80), // Center the toolbar + y: pageY - 60, + width: 160, + }); + + setIsSelecting(true); + }, + [] + ); + + /** + * Cache text layout for later position calculations + */ + const handleTextLayout = useCallback( + (block: Block, event: NativeSyntheticEvent) => { + const { lines } = event.nativeEvent; + + // Note: In real usage, also need to measure the view's page position + textLayoutCache.current[block.id] = { + lines: lines.map(line => ({ + x: line.x, + y: line.y, + width: line.width, + height: line.height, + text: line.text, + })), + pageX: 0, // Would need ref.measure() to get this + pageY: 0, + }; + }, + [] + ); + + /** + * Clear current selection + */ + const clearSelection = useCallback(() => { + setSelection(null); + setToolbarPosition(null); + setIsSelecting(false); + }, []); + + /** + * Handle highlight from rn-text-touch-highlight library + * This is the integration point for proper text selection + */ + const handleHighlight = useCallback( + (range: { start: number; end: number; text: string }, blockId: string) => { + setSelection({ + start: range.start, + end: range.end, + text: range.text, + blockId, + }); + + // Note: rn-text-touch-highlight provides position info + // For now, use a default position (would need actual position from library) + setToolbarPosition({ + x: 100, + y: 100, + width: 160, + }); + + setIsSelecting(true); + }, + [] + ); + + return { + selection, + toolbarPosition, + isSelecting, + handleLongPress, + handleTextLayout, + clearSelection, + handleHighlight, + }; +} + +/** + * Example usage with rn-text-touch-highlight: + * + * ```tsx + * import { HighlightableText } from 'rn-text-touch-highlight'; + * + * const { handleHighlight, selection } = useTextSelection(); + * + * handleHighlight(range, block.id)} + * highlights={annotations.map(a => ({ + * start: a.startOffset, + * end: a.endOffset, + * style: { backgroundColor: '#fee2e2' } + * }))} + * /> + * ``` + */ diff --git a/packages/native/index.ts b/packages/native/index.ts new file mode 100644 index 0000000..5e655ac --- /dev/null +++ b/packages/native/index.ts @@ -0,0 +1,42 @@ +/** + * @plannotator/native + * React Native components for plan review and annotation + * + * Usage in Happy: + * ```tsx + * import { PlannotatorModal } from '@plannotator/native'; + * + * setShowReview(false)} + * planMarkdown={planContent} + * onApprove={handleApprove} + * onSendFeedback={handleFeedback} + * /> + * ``` + */ + +// Re-export core types and functions +export * from '@plannotator/core'; + +// Native types +export * from './types'; + +// Components +export { PlannotatorModal } from './components/PlannotatorModal'; +export { PlanViewer } from './components/PlanViewer'; +export { BlockRenderer } from './components/BlockRenderer'; +export { CodeBlock } from './components/CodeBlock'; +export { AnnotationToolbar } from './components/AnnotationToolbar'; +export { AnnotationPanel } from './components/AnnotationPanel'; +export { InlineMarkdown } from './components/InlineMarkdown'; + +// Hooks +export { useAnnotations, createAnnotation } from './hooks/useAnnotations'; +export type { UseAnnotationsReturn } from './hooks/useAnnotations'; + +export { usePlanReview } from './hooks/usePlanReview'; +export type { UsePlanReviewReturn, ReviewResult, PlanReviewOptions } from './hooks/usePlanReview'; + +export { useTextSelection } from './hooks/useTextSelection'; +export type { UseTextSelectionReturn } from './hooks/useTextSelection'; diff --git a/packages/native/package.json b/packages/native/package.json new file mode 100644 index 0000000..ca32259 --- /dev/null +++ b/packages/native/package.json @@ -0,0 +1,19 @@ +{ + "name": "@plannotator/native", + "version": "0.1.0", + "description": "React Native components for Plannotator - plan review UI", + "main": "index.ts", + "types": "index.ts", + "exports": { + ".": "./index.ts", + "./components/*": "./components/*.tsx", + "./hooks/*": "./hooks/*.ts" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-native": ">=0.70.0" + }, + "dependencies": { + "@plannotator/core": "workspace:*" + } +} diff --git a/packages/native/types.ts b/packages/native/types.ts new file mode 100644 index 0000000..2742f94 --- /dev/null +++ b/packages/native/types.ts @@ -0,0 +1,70 @@ +/** + * @plannotator/native - Type definitions for React Native components + */ + +import type { Annotation, Block, EditorMode } from '@plannotator/core'; + +// Re-export core types +export * from '@plannotator/core'; + +// Selection range from text highlighting +export interface SelectionRange { + start: number; + end: number; + text: string; + blockId: string; +} + +// Toolbar position +export interface ToolbarPosition { + x: number; + y: number; + width: number; +} + +// Theme colors +export interface PlannotatorTheme { + background: string; + surface: string; + text: string; + textMuted: string; + primary: string; + border: string; + deletion: string; + insertion: string; + replacement: string; + comment: string; + success: string; + warning: string; +} + +// Default themes +export const lightTheme: PlannotatorTheme = { + background: '#ffffff', + surface: '#f9fafb', + text: '#1f2937', + textMuted: '#6b7280', + primary: '#6366f1', + border: '#e5e7eb', + deletion: '#fee2e2', + insertion: '#dcfce7', + replacement: '#e0e7ff', + comment: '#fef3c7', + success: '#22c55e', + warning: '#f59e0b', +}; + +export const darkTheme: PlannotatorTheme = { + background: '#1f2937', + surface: '#374151', + text: '#f9fafb', + textMuted: '#9ca3af', + primary: '#818cf8', + border: '#4b5563', + deletion: '#7f1d1d', + insertion: '#166534', + replacement: '#3730a3', + comment: '#78350f', + success: '#16a34a', + warning: '#d97706', +}; diff --git a/packages/server/annotate.ts b/packages/server/annotate.ts new file mode 100644 index 0000000..7bb8a24 --- /dev/null +++ b/packages/server/annotate.ts @@ -0,0 +1,258 @@ +/** + * Plannotator Annotation Server + * + * Simplified server for annotating arbitrary markdown files. + * Unlike the plan server, this doesn't handle hook decisions - it just outputs feedback. + * + * Usage: plannotator annotate + */ + +import { mkdirSync } from "fs"; +import { isRemoteSession, getServerPort } from "./remote"; +import { openBrowser } from "./browser"; +import { + injectValidationMarkers, + extractValidationMarkers, + type Annotation, +} from "@plannotator/core"; + +// --- Types --- + +export interface AnnotateServerOptions { + /** The markdown content to annotate */ + markdown: string; + /** Absolute path to the source file */ + filePath: string; + /** Origin identifier (e.g., "claude-code") */ + origin: string; + /** HTML content to serve for the UI */ + htmlContent: string; + /** Whether URL sharing is enabled (default: true) */ + sharingEnabled?: boolean; + /** Called when server starts with the URL, remote status, and port */ + onReady?: (url: string, isRemote: boolean, port: number) => void; +} + +export interface AnnotateServerResult { + /** The port the server is running on */ + port: number; + /** The full URL to access the server */ + url: string; + /** Whether running in remote mode */ + isRemote: boolean; + /** Wait for user to submit feedback */ + waitForDecision: () => Promise<{ feedback: string }>; + /** Stop the server */ + stop: () => void; +} + +// --- Server Implementation --- + +const MAX_RETRIES = 5; +const RETRY_DELAY_MS = 500; + +/** + * Start the Annotation server + * + * Handles: + * - Remote detection and port configuration + * - API routes for annotation mode + * - Port conflict retries + */ +export async function startAnnotateServer( + options: AnnotateServerOptions +): Promise { + const { + markdown, + filePath, + origin, + htmlContent, + sharingEnabled = true, + onReady, + } = options; + + const isRemote = isRemoteSession(); + const configuredPort = getServerPort(); + + // Decision promise - resolves when user submits feedback + let resolveDecision: (result: { feedback: string }) => void; + const decisionPromise = new Promise<{ feedback: string }>((resolve) => { + resolveDecision = resolve; + }); + + // Start server with retry logic + let server: ReturnType | null = null; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + server = Bun.serve({ + port: configuredPort, + + async fetch(req) { + const url = new URL(req.url); + + // API: Get markdown content (same endpoint as plan for UI compatibility) + if (url.pathname === "/api/plan") { + // Extract existing validation markers from the markdown + const existingMarkers = extractValidationMarkers(markdown); + + return Response.json({ + plan: markdown, + origin, + filePath, + mode: "annotate", + sharingEnabled, + existingMarkers, + }); + } + + // API: Serve images (local paths or temp uploads) + if (url.pathname === "/api/image") { + const imagePath = url.searchParams.get("path"); + if (!imagePath) { + return new Response("Missing path parameter", { status: 400 }); + } + try { + const file = Bun.file(imagePath); + if (!(await file.exists())) { + return new Response("File not found", { status: 404 }); + } + return new Response(file); + } catch { + return new Response("Failed to read file", { status: 500 }); + } + } + + // API: Upload image -> save to temp -> return path + if (url.pathname === "/api/upload" && req.method === "POST") { + try { + const formData = await req.formData(); + const file = formData.get("file") as File; + if (!file) { + return new Response("No file provided", { status: 400 }); + } + + const ext = file.name.split(".").pop() || "png"; + const tempDir = "/tmp/plannotator"; + mkdirSync(tempDir, { recursive: true }); + const tempPath = `${tempDir}/${crypto.randomUUID()}.${ext}`; + + await Bun.write(tempPath, file); + return Response.json({ path: tempPath }); + } catch (err) { + const message = + err instanceof Error ? err.message : "Upload failed"; + return Response.json({ error: message }, { status: 500 }); + } + } + + // API: Save validation markers to source file + if (url.pathname === "/api/save-markers" && req.method === "POST") { + try { + const body = (await req.json()) as { annotations?: Annotation[] }; + const annotations = body.annotations || []; + + // Inject validation markers into the markdown + const result = injectValidationMarkers(markdown, annotations); + + if (result.markersAdded === 0) { + return Response.json({ + success: true, + markersAdded: 0, + message: "No validation markers to add", + }); + } + + // Write the modified markdown back to the source file + await Bun.write(filePath, result.markdown); + + return Response.json({ + success: true, + markersAdded: result.markersAdded, + markers: result.markers, + message: `Added ${result.markersAdded} validation marker(s) to ${filePath}`, + }); + } catch (err) { + const message = + err instanceof Error ? err.message : "Failed to save markers"; + return Response.json({ success: false, error: message }, { status: 500 }); + } + } + + // API: Submit feedback (annotation mode endpoint) + if (url.pathname === "/api/feedback" && req.method === "POST") { + let feedback = ""; + try { + const body = (await req.json()) as { feedback?: string }; + feedback = body.feedback || ""; + } catch { + // Empty feedback + } + + resolveDecision({ feedback }); + return Response.json({ ok: true }); + } + + // API: Also handle /api/deny as feedback submission (UI compatibility) + if (url.pathname === "/api/deny" && req.method === "POST") { + let feedback = ""; + try { + const body = (await req.json()) as { feedback?: string }; + feedback = body.feedback || ""; + } catch { + // Empty feedback + } + + resolveDecision({ feedback }); + return Response.json({ ok: true }); + } + + // Serve embedded HTML for all other routes (SPA) + return new Response(htmlContent, { + headers: { "Content-Type": "text/html" }, + }); + }, + }); + + break; // Success, exit retry loop + } catch (err: unknown) { + const isAddressInUse = + err instanceof Error && err.message.includes("EADDRINUSE"); + + if (isAddressInUse && attempt < MAX_RETRIES) { + await Bun.sleep(RETRY_DELAY_MS); + continue; + } + + if (isAddressInUse) { + const hint = isRemote + ? " (set PLANNOTATOR_PORT to use different port)" + : ""; + throw new Error( + `Port ${configuredPort} in use after ${MAX_RETRIES} retries${hint}` + ); + } + + throw err; + } + } + + if (!server) { + throw new Error("Failed to start server"); + } + + const serverUrl = `http://localhost:${server.port}`; + + // Notify caller that server is ready + if (onReady) { + onReady(serverUrl, isRemote, server.port); + } + + return { + port: server.port, + url: serverUrl, + isRemote, + waitForDecision: () => decisionPromise, + stop: () => server.stop(), + }; +} diff --git a/packages/server/index.ts b/packages/server/index.ts index e35e5bd..b7c75b3 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -24,8 +24,25 @@ import { savePlan, saveAnnotations, saveFinalSnapshot, + extractFirstHeading, } from "./storage"; +// --- Version Tracking --- +// Track plan versions by title (H1) within a session +const planVersions = new Map(); + +/** + * Get the version number for a plan based on its title. + * Increments version each time the same title is seen. + */ +function getPlanVersion(plan: string): number { + const heading = extractFirstHeading(plan) || "untitled"; + const currentVersion = planVersions.get(heading) || 0; + const newVersion = currentVersion + 1; + planVersions.set(heading, newVersion); + return newVersion; +} + // Re-export utilities export { isRemoteSession, getServerPort } from "./remote"; export { openBrowser } from "./browser"; @@ -93,6 +110,11 @@ export async function startPlannotatorServer( // Generate slug for potential saving (actual save happens on decision) const slug = generateSlug(plan); + // Extract metadata for UI display + const planTitle = extractFirstHeading(plan) || "Untitled Plan"; + const planVersion = getPlanVersion(plan); + const planTimestamp = new Date().toISOString(); + // Decision promise let resolveDecision: (result: { approved: boolean; @@ -124,7 +146,16 @@ export async function startPlannotatorServer( // API: Get plan content if (url.pathname === "/api/plan") { - return Response.json({ plan, origin, permissionMode, sharingEnabled }); + return Response.json({ + plan, + origin, + permissionMode, + sharingEnabled, + // Metadata for UI header + title: planTitle, + version: planVersion, + timestamp: planTimestamp, + }); } // API: Serve images (local paths or temp uploads) diff --git a/packages/server/package.json b/packages/server/package.json index d078699..73259c5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -8,6 +8,7 @@ "exports": { ".": "./index.ts", "./review": "./review.ts", + "./annotate": "./annotate.ts", "./remote": "./remote.ts", "./browser": "./browser.ts", "./storage": "./storage.ts", @@ -16,6 +17,9 @@ "files": [ "*.ts" ], + "dependencies": { + "@plannotator/core": "workspace:*" + }, "peerDependencies": { "bun": ">=1.0.0" } diff --git a/packages/server/project.test.ts b/packages/server/project.test.ts index 78e9d4d..5aced3b 100644 --- a/packages/server/project.test.ts +++ b/packages/server/project.test.ts @@ -105,10 +105,10 @@ describe("detectProjectName", () => { } }); - // This test verifies we get "planning-hook" when run from this repo + // This test verifies we get "plannotator" when run from this repo test("detects current repo name", async () => { const result = await detectProjectName(); - // We're in the planning-hook repo, so should get that name - expect(result).toBe("planning-hook"); + // We're in the plannotator repo, so should get that name + expect(result).toBe("plannotator"); }); }); diff --git a/packages/server/storage.ts b/packages/server/storage.ts index ec9321d..d627585 100644 --- a/packages/server/storage.ts +++ b/packages/server/storage.ts @@ -34,7 +34,7 @@ export function getPlanDir(customPath?: string | null): string { /** * Extract the first heading from markdown content. */ -function extractFirstHeading(markdown: string): string | null { +export function extractFirstHeading(markdown: string): string | null { const match = markdown.match(/^#\s+(.+)$/m); if (!match) return null; return match[1].trim(); diff --git a/packages/ui/components/AnnotationPanel.tsx b/packages/ui/components/AnnotationPanel.tsx index 537a6f5..da167c1 100644 --- a/packages/ui/components/AnnotationPanel.tsx +++ b/packages/ui/components/AnnotationPanel.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; -import { Annotation, AnnotationType, Block } from '../types'; +import { Annotation, AnnotationType, Block, ReviewTag } from '../types'; import { isCurrentUser } from '../utils/identity'; import { ImageThumbnail } from './ImageThumbnail'; @@ -27,7 +27,7 @@ export const AnnotationPanel: React.FC = ({ sharingEnabled = true }) => { const [copied, setCopied] = useState(false); - const sortedAnnotations = [...annotations].sort((a, b) => a.createdA - b.createdA); + const sortedAnnotations = [...annotations].sort((a, b) => a.createdAt - b.createdAt); const handleQuickShare = async () => { if (!shareUrl) return; @@ -267,7 +267,7 @@ const AnnotationCard: React.FC<{ )} - {/* Type Badge + Timestamp + Actions */} + {/* Type Badge + Tag + Timestamp + Actions */}
@@ -278,8 +278,18 @@ const AnnotationCard: React.FC<{ {config.label}
+ {annotation.tag && ( + + {annotation.tag} + + )} + {annotation.isMacro && ( + + [MACRO] + + )} - {formatTimestamp(annotation.createdA)} + {formatTimestamp(annotation.createdAt)}
diff --git a/packages/ui/components/AnnotationToolbar.tsx b/packages/ui/components/AnnotationToolbar.tsx index cde89c4..81d87b5 100644 --- a/packages/ui/components/AnnotationToolbar.tsx +++ b/packages/ui/components/AnnotationToolbar.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { AnnotationType } from "../types"; +import { AnnotationType, ReviewTag, REVIEW_TAG_CATEGORIES } from "../types"; import { createPortal } from "react-dom"; import { AttachmentsButton } from "./AttachmentsButton"; @@ -8,7 +8,7 @@ type PositionMode = 'center-above' | 'top-right'; interface AnnotationToolbarProps { element: HTMLElement; positionMode: PositionMode; - onAnnotate: (type: AnnotationType, text?: string, imagePaths?: string[]) => void; + onAnnotate: (type: AnnotationType, text?: string, imagePaths?: string[], tag?: ReviewTag, isMacro?: boolean) => void; onClose: () => void; /** Text to copy (for text selection, pass source.text) */ copyText?: string; @@ -38,9 +38,13 @@ export const AnnotationToolbar: React.FC = ({ const [activeType, setActiveType] = useState(null); const [inputValue, setInputValue] = useState(""); const [imagePaths, setImagePaths] = useState([]); + const [selectedTag, setSelectedTag] = useState(undefined); + const [isMacro, setIsMacro] = useState(false); + const [showTagDropdown, setShowTagDropdown] = useState(false); const [position, setPosition] = useState<{ top: number; left?: number; right?: number } | null>(null); const [copied, setCopied] = useState(false); const inputRef = useRef(null); + const tagDropdownRef = useRef(null); const handleCopy = async () => { // Use provided copyText, or fall back to code element / element text @@ -65,9 +69,25 @@ export const AnnotationToolbar: React.FC = ({ setActiveType(null); setInputValue(""); setImagePaths([]); + setSelectedTag(undefined); + setIsMacro(false); + setShowTagDropdown(false); setCopied(false); }, [element]); + // Close tag dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (tagDropdownRef.current && !tagDropdownRef.current.contains(e.target as Node)) { + setShowTagDropdown(false); + } + }; + if (showTagDropdown) { + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + } + }, [showTagDropdown]); + // Notify parent when locked (in input mode) useEffect(() => { onLockChange?.(step === "input"); @@ -121,7 +141,7 @@ export const AnnotationToolbar: React.FC = ({ const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (activeType && (inputValue.trim() || imagePaths.length > 0)) { - onAnnotate(activeType, inputValue || undefined, imagePaths.length > 0 ? imagePaths : undefined); + onAnnotate(activeType, inputValue || undefined, imagePaths.length > 0 ? imagePaths : undefined, selectedTag, isMacro || undefined); } }; @@ -186,7 +206,90 @@ export const AnnotationToolbar: React.FC = ({ />
) : ( -
+ + {/* Tag Selector Dropdown */} +
+ + {showTagDropdown && ( +
+ +
+
Modification
+ {REVIEW_TAG_CATEGORIES.modification.map(tag => ( + + ))} +
+
Vérification
+ {REVIEW_TAG_CATEGORIES.verification.map(tag => ( + + ))} +
+
Validation
+ {REVIEW_TAG_CATEGORIES.validation.map(tag => ( + + ))} +
+ )} +
+ {/* [MACRO] Toggle */} +