diff --git a/docs/architecture.md b/docs/architecture.md index 1986f30..59baa80 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,7 +60,7 @@ That keeps the viewer static-hosting friendly while removing the brittle parts o ## Bundle tradeoffs -The largest remaining deferred cost is still the diff renderer stack, primarily `@git-diff-view/*` and its highlighting internals. Its vendor stylesheet is served from `public/vendor/diff-view-pure.css` with a precompressed `.css.br` variant and injected only when a rich diff mounts, so the empty shell and non-diff artifacts do not pay that CSS cost. The stack remains because it still provides the best review-style UX for multi-file git patches, split/unified modes, and syntax-aware rendering with less product code than a bespoke replacement. +The largest remaining deferred cost is still the diff renderer stack, now `@pierre/diffs` with its Shiki grammars. There is no vendor stylesheet: styles ship inside the library's shadow roots, so the empty shell and non-diff artifacts pay no diff CSS or JS cost at all — the whole stack loads only when a diff artifact mounts, behind the existing `next/dynamic` boundary. The JSON and markdown paths are now substantially lighter because: @@ -71,12 +71,13 @@ The JSON and markdown paths are now substantially lighter because: ## Diff choice -`agent-render` uses `@git-diff-view/react` plus git-diff `DiffFile` instances instead of `@codemirror/merge`. +`agent-render` uses `@pierre/diffs` (`PatchDiff` per parsed patch file, `MultiFileDiff` for before/after contents) instead of `@codemirror/merge`. -- `@git-diff-view/*` matches the product goal better because it is already shaped like a GitHub-style review surface -- split and unified views are built in -- syntax highlighting and diff affordances are stronger out of the box for artifact viewing -- individual file patches can be rendered as a sequence while preserving filenames and boundaries +- `@pierre/diffs` matches the product goal better because it is already shaped like a review surface +- split and unified views are built in (`diffStyle` option) +- Shiki-based syntax highlighting is stronger out of the box for artifact viewing +- individual file patches can be rendered as a sequence while preserving filenames and boundaries via the repo's own `parseGitPatchBundle` +- before/after content diffs are computed by the library itself, which the previous stack could not do from raw contents - CodeMirror remains the better fit for full source artifacts and markdown code fences `@codemirror/merge` stays a reasonable future option if the project ever needs a more editor-centric comparison workflow, but it is not the best default for shareable review artifacts. diff --git a/docs/dependency-notes.md b/docs/dependency-notes.md index c17ef1e..43a1166 100644 --- a/docs/dependency-notes.md +++ b/docs/dependency-notes.md @@ -10,7 +10,7 @@ - `rehype-sanitize` - MIT - `@codemirror/*` - MIT - `@replit/codemirror-indentation-markers` - MIT -- `@git-diff-view/*` - MIT +- `@pierre/diffs` - Apache-2.0 - `papaparse` - MIT - `lz-string` - MIT - `fflate` - MIT @@ -30,7 +30,7 @@ - `next` pins its nested `postcss` dependency to `8.5.14` via `package.json` overrides so Tailwind CSS v4's `postcss ^8.5.6` peer range is satisfied in the Next.js toolchain. - CodeMirror handles source artifacts and markdown code fences because it is excellent at read-only code presentation; JSON, markdown raw, and CSV raw views use lighter native source blocks. - `@replit/codemirror-indentation-markers` replaces custom indent-guide logic with a maintained CM6 extension. -- `@git-diff-view/*` fits review-style diffs better than a generic merge editor for the current viewer. Its pure CSS file is mirrored into `public/vendor/diff-view-pure.css` with a Brotli-compressed `public/vendor/diff-view-pure.css.br` copy by `npm run assets:compress`, and loaded only by the diff renderer; `tests/diff-style-asset.test.ts` keeps those assets in sync with the package copy. +- `@pierre/diffs` (diffs.com) renders review-style diffs from patch strings (`PatchDiff`) or raw before/after contents (`MultiFileDiff`). It is Shiki-based, styles itself inside shadow DOM (no vendor stylesheet to mirror or serve), and is themed through `--diffs-*` CSS custom properties set on `.diff-renderer-frame`. It is imported only through the `src/lib/diff/pierre-react.ts` bridge so the deferred chunk stays stable across webpack graphs. - `papaparse` handles CSV parsing; CSV rendering uses a native read-only table to avoid a data-grid dependency for the shipped static viewer. - `fflate` provides portable deflate/inflate support across iOS Safari and Android Chromium without relying on browser-specific compression streams. - `brotli-wasm` provides the arx/arx2/arx3 Brotli compression layer, including streaming decompression used to cap expanded output before allocating oversized decoded payloads. diff --git a/docs/testing.md b/docs/testing.md index ed1857c..8d326c7 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -47,7 +47,7 @@ The suite is intentionally split by responsibility: - visual tests protect empty state, artifact views, theme presentation, and compact-content spacing - component tests protect selector/disclosure UI contracts - unit tests protect transport codecs, envelope validation, diff parsing, and language inference -- `npm run assets:compress` regenerates minified/precompressed public assets, including the ARX dictionaries and mirrored diff-view stylesheet +- `npm run assets:compress` regenerates minified/precompressed public assets (the ARX dictionaries) - `npm run bench:codecs` protects arx/arx2 compressed-byte ratios and arx3 visible-character wins against the committed `scripts/bench-baseline.json`; its corpus is fixed in `scripts/bench-codecs.mjs` so unrelated source, docs, or package metadata edits do not create false codec regressions - `npm run check:build-budgets` reads the generated `.next` manifests after `npm run build` and fails if the homepage shell or key deferred renderer chunks exceed their gzip budgets diff --git a/package-lock.json b/package-lock.json index d2d522b..d4caaf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@codemirror/language": "^6.12.4", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.38.2", - "@git-diff-view/react": "^0.1.6", + "@pierre/diffs": "^1.3.5", "@replit/codemirror-indentation-markers": "^6.5.3", "brotli-wasm": "^3.0.1", "fflate": "^0.8.2", @@ -1126,48 +1126,6 @@ } } }, - "node_modules/@git-diff-view/core": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@git-diff-view/core/-/core-0.1.6.tgz", - "integrity": "sha512-q2Ch8jURF6pL7VeNpOgHBRVY9gsGLXCOYpKXHG3BqpXe0kv6GNSUux8SmAYsDrakBzfgDClODxDtsM2rfiWpnA==", - "license": "MIT", - "dependencies": { - "@git-diff-view/lowlight": "^0.1.6", - "fast-diff": "^1.3.0", - "highlight.js": "^11.11.0", - "lowlight": "^3.3.0" - } - }, - "node_modules/@git-diff-view/lowlight": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@git-diff-view/lowlight/-/lowlight-0.1.6.tgz", - "integrity": "sha512-YIsiAc2aWAePWaDNi3k8xI0Vs/ZItt5J6nrftTIFbMFN3GwDOsyJFm2L7o8XWKTJkV2yItaz28KUI9CWj0MVZA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "highlight.js": "^11.11.0", - "lowlight": "^3.3.0" - } - }, - "node_modules/@git-diff-view/react": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@git-diff-view/react/-/react-0.1.6.tgz", - "integrity": "sha512-koABBon5bNKh6/WnWSxggK9ojw+cvWAPnY2/ciOkwlR+8dm0h6A7Qa5kP2HFDxqYHwZ2imkGMcSLgXMOnWHRFA==", - "license": "MIT", - "dependencies": { - "@git-diff-view/core": "^0.1.6", - "@types/hast": "^3.0.0", - "fast-diff": "^1.3.0", - "highlight.js": "^11.11.0", - "lowlight": "^3.3.0", - "reactivity-store": "^0.4.0", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2073,6 +2031,64 @@ "node": ">=12.4.0" } }, + "node_modules/@pierre/diffs": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.3.5.tgz", + "integrity": "sha512-BhaLEiUvR+BdIyOYdogA4JLQjluWPubuwySmmIqEkcE0FwIWRbgJgHNC/r884dlxEr89fO4hTk/sBln0a7NSOw==", + "license": "apache-2.0", + "dependencies": { + "@pierre/theme": "2.0.0", + "@pierre/theming": "1.0.1", + "@shikijs/transformers": "^3.0.0 || ^4.0.0", + "diff": "9.0.0", + "hast-util-to-html": "9.0.5", + "lru_map": "0.4.1", + "shiki": "^3.0.0 || ^4.0.0" + }, + "peerDependencies": { + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0" + } + }, + "node_modules/@pierre/theme": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pierre/theme/-/theme-2.0.0.tgz", + "integrity": "sha512-yNDd9GYLQl1mEUJR8AneJ5e4ohLIHQd/wZLWr4fagt78vS2RwwZNW530vVgHqXFAyFVcFlRmGUD5ramXH46OXw==", + "license": "apache-2.0", + "engines": { + "vscode": "^1.0.0" + } + }, + "node_modules/@pierre/theming": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pierre/theming/-/theming-1.0.1.tgz", + "integrity": "sha512-WCI5Qd7iprDpISL9fBYOLe8RV53+b7mFNA3bPzl60/2CKCSrsKN8zEcep6Y3BAzvARlmca50zGjDodqPGiTUKA==", + "license": "apache-2.0", + "peerDependencies": { + "@pierre/theme": "^1.1.0 || ^2.0.0", + "@shikijs/themes": "^3.0.0 || ^4.0.0", + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0", + "shiki": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@pierre/theme": { + "optional": true + }, + "@shikijs/themes": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "shiki": { + "optional": true + } + } + }, "node_modules/@playwright/test": { "version": "1.61.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", @@ -2464,6 +2480,119 @@ "dev": true, "license": "MIT" }, + "node_modules/@shikijs/core": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.2.tgz", + "integrity": "sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.4.2", + "@shikijs/types": "4.4.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.2.tgz", + "integrity": "sha512-MnIkeqWdVPUWsxlx8gKLVCJFTsqrQJgpTPBPpQwaFeJ56lOnJxj5aN2LUFnfxUEcvOQuNocmbaMnVrCEln6rkw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.2.tgz", + "integrity": "sha512-GLhowz1+jixjz+wiZ3wMnOn1jTxiFCGl2PkXufivbnwPHKuyw1AYqu5/hbWhZZ2oAb0NP05WUJhYeigY14drnw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.2", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.2.tgz", + "integrity": "sha512-8DfeusD+Zdv/eYIDdXyJTUnSMHt+aAWjAOCXV20HNGAHRlInXpG8wh421v6B91WOm9TFwRLN+b/LG5F2NAIojg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.2.tgz", + "integrity": "sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.2.tgz", + "integrity": "sha512-H0CFoL07ddDC2Dd6EdrPYNkRhUR6YCkJlnuYFceYYUJJA5TIm2b5B33qqiDYryBExgbKMndFJPb2u1gTuqO37g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/transformers": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-4.4.2.tgz", + "integrity": "sha512-d81PJ9KkR1tVP95FH/9296HTtDo0mh76wv10u9T1YmsZq/UcXgt0OLdBszfUQ1i+umkRMCjDnFbFZU7/tCODTQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.2", + "@shikijs/types": "4.4.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.2.tgz", + "integrity": "sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3247,9 +3376,9 @@ "license": "MIT" }, "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -4016,21 +4145,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vue/reactivity": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz", - "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.38" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz", - "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", - "license": "MIT" - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -5519,6 +5633,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -6341,12 +6464,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "license": "Apache-2.0" - }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -6822,6 +6939,29 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -6862,15 +7002,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -6894,6 +7025,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -8057,20 +8198,11 @@ "loose-envify": "cli.js" } }, - "node_modules/lowlight": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", - "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.0.0", - "highlight.js": "~11.11.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/lru_map": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", + "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==", + "license": "MIT" }, "node_modules/lru-cache": { "version": "11.2.6", @@ -9404,6 +9536,23 @@ "wrappy": "1" } }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9925,20 +10074,6 @@ "react": ">=18" } }, - "node_modules/reactivity-store": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/reactivity-store/-/reactivity-store-0.4.0.tgz", - "integrity": "sha512-uL9uoREOBg2o4zUa8vMU0AbvAOk0osPloizscmyZqMvJzcuuKX3ELFYYr1DX8gAcfvlhPduz4QuLZn1eChCu4Q==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "~3.5.30", - "@vue/shared": "~3.5.30", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -9991,6 +10126,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -10478,6 +10637,25 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.2.tgz", + "integrity": "sha512-P8F/dFhRevaw2uSdeIYlq/5SXZNY85DPtmXQ947gD1Zj2JqO5AkNvVVBar0Me9JkFx3uzVud/qOtP5ek9NEQGA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.2", + "@shikijs/engine-javascript": "4.4.2", + "@shikijs/engine-oniguruma": "4.4.2", + "@shikijs/langs": "4.4.2", + "@shikijs/themes": "4.4.2", + "@shikijs/types": "4.4.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -11961,15 +12139,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 24d1161..fbc49b1 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@codemirror/language": "^6.12.4", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.38.2", - "@git-diff-view/react": "^0.1.6", + "@pierre/diffs": "^1.3.5", "@replit/codemirror-indentation-markers": "^6.5.3", "brotli-wasm": "^3.0.1", "fflate": "^0.8.2", diff --git a/public/_headers b/public/_headers index f2ce36d..0a1b2ee 100644 --- a/public/_headers +++ b/public/_headers @@ -19,8 +19,3 @@ Content-Type: application/json; charset=utf-8 Content-Encoding: br Vary: Accept-Encoding - -/vendor/diff-view-pure.css.br - Content-Type: text/css; charset=utf-8 - Content-Encoding: br - Vary: Accept-Encoding diff --git a/public/vendor/diff-view-pure.css b/public/vendor/diff-view-pure.css deleted file mode 100644 index 9c6e072..0000000 --- a/public/vendor/diff-view-pure.css +++ /dev/null @@ -1,771 +0,0 @@ -.diff-tailwindcss-wrapper .\!container { - width: 100% !important; -} -.diff-tailwindcss-wrapper .container { - width: 100%; -} -@media (min-width: 640px) { - .diff-tailwindcss-wrapper .\!container { - max-width: 640px !important; - } - .diff-tailwindcss-wrapper .container { - max-width: 640px; - } -} -@media (min-width: 768px) { - .diff-tailwindcss-wrapper .\!container { - max-width: 768px !important; - } - .diff-tailwindcss-wrapper .container { - max-width: 768px; - } -} -@media (min-width: 1024px) { - .diff-tailwindcss-wrapper .\!container { - max-width: 1024px !important; - } - .diff-tailwindcss-wrapper .container { - max-width: 1024px; - } -} -@media (min-width: 1280px) { - .diff-tailwindcss-wrapper .\!container { - max-width: 1280px !important; - } - .diff-tailwindcss-wrapper .container { - max-width: 1280px; - } -} -@media (min-width: 1536px) { - .diff-tailwindcss-wrapper .\!container { - max-width: 1536px !important; - } - .diff-tailwindcss-wrapper .container { - max-width: 1536px; - } -} -.diff-tailwindcss-wrapper .invisible { - visibility: hidden; -} -.diff-tailwindcss-wrapper .absolute { - position: absolute; -} -.diff-tailwindcss-wrapper .relative { - position: relative; -} -.diff-tailwindcss-wrapper .sticky { - position: sticky; -} -.diff-tailwindcss-wrapper .left-0 { - left: 0px; -} -.diff-tailwindcss-wrapper .left-\[100\%\] { - left: 100%; -} -.diff-tailwindcss-wrapper .right-\[100\%\] { - right: 100%; -} -.diff-tailwindcss-wrapper .top-0 { - top: 0px; -} -.diff-tailwindcss-wrapper .top-\[50\%\] { - top: 50%; -} -.diff-tailwindcss-wrapper .z-\[1\] { - z-index: 1; -} -.diff-tailwindcss-wrapper .ml-\[-1\.5em\] { - margin-left: -1.5em; -} -.diff-tailwindcss-wrapper .block { - display: block; -} -.diff-tailwindcss-wrapper .inline-block { - display: inline-block; -} -.diff-tailwindcss-wrapper .flex { - display: flex; -} -.diff-tailwindcss-wrapper .table { - display: table; -} -.diff-tailwindcss-wrapper .hidden { - display: none; -} -.diff-tailwindcss-wrapper .h-\[50\%\] { - height: 50%; -} -.diff-tailwindcss-wrapper .h-full { - height: 100%; -} -.diff-tailwindcss-wrapper .min-h-\[28px\] { - min-height: 28px; -} -.diff-tailwindcss-wrapper .w-\[1\%\] { - width: 1%; -} -.diff-tailwindcss-wrapper .w-\[1\.5em\] { - width: 1.5em; -} -.diff-tailwindcss-wrapper .w-\[1\.5px\] { - width: 1.5px; -} -.diff-tailwindcss-wrapper .w-\[10px\] { - width: 10px; -} -.diff-tailwindcss-wrapper .w-\[1px\] { - width: 1px; -} -.diff-tailwindcss-wrapper .w-\[50\%\] { - width: 50%; -} -.diff-tailwindcss-wrapper .w-full { - width: 100%; -} -.diff-tailwindcss-wrapper .w-max { - width: -moz-max-content; - width: max-content; -} -.diff-tailwindcss-wrapper .min-w-\[100px\] { - min-width: 100px; -} -.diff-tailwindcss-wrapper .min-w-\[40px\] { - min-width: 40px; -} -.diff-tailwindcss-wrapper .min-w-full { - min-width: 100%; -} -.diff-tailwindcss-wrapper .flex-shrink-0 { - flex-shrink: 0; -} -.diff-tailwindcss-wrapper .shrink-0 { - flex-shrink: 0; -} -.diff-tailwindcss-wrapper .basis-\[50\%\] { - flex-basis: 50%; -} -.diff-tailwindcss-wrapper .table-fixed { - table-layout: fixed; -} -.diff-tailwindcss-wrapper .border-collapse { - border-collapse: collapse; -} -.diff-tailwindcss-wrapper .border-spacing-0 { - --tw-border-spacing-x: 0px; - --tw-border-spacing-y: 0px; - border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y); -} -.diff-tailwindcss-wrapper .origin-center { - transform-origin: center; -} -.diff-tailwindcss-wrapper .translate-x-\[-50\%\] { - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.diff-tailwindcss-wrapper .translate-x-\[50\%\] { - --tw-translate-x: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.diff-tailwindcss-wrapper .translate-y-\[-50\%\] { - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.diff-tailwindcss-wrapper .transform { - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.diff-tailwindcss-wrapper .cursor-pointer { - cursor: pointer; -} -.diff-tailwindcss-wrapper .select-none { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} -.diff-tailwindcss-wrapper .flex-col { - flex-direction: column; -} -.diff-tailwindcss-wrapper .items-start { - align-items: flex-start; -} -.diff-tailwindcss-wrapper .items-center { - align-items: center; -} -.diff-tailwindcss-wrapper .justify-center { - justify-content: center; -} -.diff-tailwindcss-wrapper .overflow-x-auto { - overflow-x: auto; -} -.diff-tailwindcss-wrapper .overflow-y-hidden { - overflow-y: hidden; -} -.diff-tailwindcss-wrapper .whitespace-nowrap { - white-space: nowrap; -} -.diff-tailwindcss-wrapper .break-all { - word-break: break-all; -} -.diff-tailwindcss-wrapper .rounded-\[2px\] { - border-radius: 2px; -} -.diff-tailwindcss-wrapper .rounded-md { - border-radius: 0.375rem; -} -.diff-tailwindcss-wrapper .border-l-\[1px\] { - border-left-width: 1px; -} -.diff-tailwindcss-wrapper .fill-current { - fill: currentColor; -} -.diff-tailwindcss-wrapper .p-0 { - padding: 0px; -} -.diff-tailwindcss-wrapper .p-\[1px\] { - padding: 1px; -} -.diff-tailwindcss-wrapper .px-\[10px\] { - padding-left: 10px; - padding-right: 10px; -} -.diff-tailwindcss-wrapper .py-\[2px\] { - padding-top: 2px; - padding-bottom: 2px; -} -.diff-tailwindcss-wrapper .py-\[6px\] { - padding-top: 6px; - padding-bottom: 6px; -} -.diff-tailwindcss-wrapper .pl-\[1\.5em\] { - padding-left: 1.5em; -} -.diff-tailwindcss-wrapper .pl-\[10px\] { - padding-left: 10px; -} -.diff-tailwindcss-wrapper .pl-\[2\.0em\] { - padding-left: 2.0em; -} -.diff-tailwindcss-wrapper .pr-\[10px\] { - padding-right: 10px; -} -.diff-tailwindcss-wrapper .text-right { - text-align: right; -} -.diff-tailwindcss-wrapper .indent-\[0\.2em\] { - text-indent: 0.2em; -} -.diff-tailwindcss-wrapper .align-top { - vertical-align: top; -} -.diff-tailwindcss-wrapper .align-middle { - vertical-align: middle; -} -.diff-tailwindcss-wrapper .text-\[1\.2em\] { - font-size: 1.2em; -} -.diff-tailwindcss-wrapper .leading-\[1\.6\] { - line-height: 1.6; -} -.diff-tailwindcss-wrapper .\!text-red-500 { - --tw-text-opacity: 1 !important; - color: rgb(239 68 68 / var(--tw-text-opacity, 1)) !important; -} -.diff-tailwindcss-wrapper .opacity-\[0\.5\] { - opacity: 0.5; -} -.diff-tailwindcss-wrapper .transition-transform { - transition-property: transform; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} -.diff-tailwindcss-wrapper * { - box-sizing: border-box; -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-style-root { - --diff-border--: #dedede; - --diff-add-content--: #dafbe1; - --diff-del-content--: #ffebe9; - --diff-add-lineNumber--: #aceebb; - --diff-del-lineNumber--: #ffcecb; - --diff-plain-content--: #ffffff; - --diff-expand-content--: #fafafa; - --diff-plain-lineNumber--: #fafafa; - --diff-expand-lineNumber--: #fafafa; - --diff-plain-lineNumber-color--: #555555; - --diff-expand-lineNumber-color--: #555555; - --diff-hunk-content--: #ddf4ff; - --diff-hunk-lineNumber--: #b6e3ff; - --diff-hunk-lineNumber-hover--: #0969da; - --diff-add-content-highlight--: #aceebb; - --diff-del-content-highlight--: #ffcecb; - --diff-add-widget--: #0969d2; - --diff-add-widget-color--: #ffffff; - --diff-empty-content--: #fafafa; - --diff-hunk-content-color--: #777777; - - color: black; -} -.diff-tailwindcss-wrapper .diff-style-root .diff-line-syntax-raw *, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw * { - color: var(--diff-view-light, inherit); - font-weight: var(--diff-view-light-font-weight, inherit); -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-style-root { - --diff-border--: #3d444d; - --diff-add-content--: #18271f; - --diff-del-content--: #23191c; - --diff-add-lineNumber--: #284228; - --diff-del-lineNumber--: #4f2828; - --diff-plain-content--: #0d1117; - --diff-expand-content--: #161b22; - --diff-plain-lineNumber--: #161b22; - --diff-expand-lineNumber--: #161b22; - --diff-plain-lineNumber-color--: #a0aaab; - --diff-expand-lineNumber-color--: #a0aaab; - --diff-hunk-content--: #131d2e; - --diff-hunk-lineNumber--: #0c2d6b; - --diff-hunk-lineNumber-hover--: #1f6feb; - --diff-add-content-highlight--: #2f5732; - --diff-del-content-highlight--: #713431; - --diff-add-widget--: #0969d2; - --diff-add-widget-color--: #ffffff; - --diff-empty-content--: #161b22; - --diff-hunk-content-color--: #9298a0; - - color: white; -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw * { - color: var(--diff-view-dark, inherit); - font-weight: var(--diff-view-dark-font-weight, inherit); -} -.diff-tailwindcss-wrapper table, -.diff-tailwindcss-wrapper tr, -.diff-tailwindcss-wrapper td { - border-color: transparent; - border-width: 0px; - text-align: left; -} -.diff-tailwindcss-wrapper td { - padding: 0; -} -.diff-tailwindcss-wrapper .diff-line-old-num, -.diff-tailwindcss-wrapper .diff-line-new-num, -.diff-tailwindcss-wrapper .diff-line-num { - text-align: right; -} -.diff-tailwindcss-wrapper .diff-style-root tr { - content-visibility: auto; -} -.diff-tailwindcss-wrapper .diff-add-widget-wrapper { - transform-origin: center; - transform: translateX(-50%) !important; -} -.diff-tailwindcss-wrapper .diff-line-old-content .diff-add-widget-wrapper, -.diff-tailwindcss-wrapper .diff-line-new-content .diff-add-widget-wrapper { - transform: translateX(50%) !important; -} -.diff-tailwindcss-wrapper .diff-add-widget-wrapper:hover { - transform: translateX(-50%) scale(1.1) !important; -} -.diff-tailwindcss-wrapper .diff-line-old-content .diff-add-widget-wrapper:hover, -.diff-tailwindcss-wrapper .diff-line-new-content .diff-add-widget-wrapper:hover { - transform: translateX(50%) scale(1.1) !important; -} -.diff-tailwindcss-wrapper .diff-widget-tooltip { - position: relative; -} -.diff-tailwindcss-wrapper .diff-add-widget, -.diff-tailwindcss-wrapper .diff-widget-tooltip { - font-family: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - font-size: 100%; - font-weight: inherit; - line-height: inherit; - letter-spacing: inherit; - color: inherit; - margin: 0; - text-transform: none; - border-width: 0px; - background-color: transparent; - background-image: none; -} -.diff-tailwindcss-wrapper .diff-widget-tooltip::after { - display: none; - box-sizing: border-box; - background-color: #555555; - position: absolute; - content: attr(data-title); - font-size: 11px; - padding: 1px 2px; - border-radius: 4px; - overflow: hidden; - top: 50%; - white-space: nowrap; - transform: translateY(-50%); - left: calc(100% + 8px); - color: #ffffff; -} -.diff-tailwindcss-wrapper .diff-widget-tooltip::before { - display: none; - box-sizing: border-box; - content: ""; - position: absolute; - top: 50%; - left: calc(100% - 2px); - transform: translateY(-50%); - border: 6px solid transparent; - border-right-color: #555555; -} -.diff-tailwindcss-wrapper .diff-widget-tooltip:hover { - background-color: var(--diff-hunk-lineNumber-hover--); - color: white; -} -.diff-tailwindcss-wrapper .diff-widget-tooltip:hover::before { - display: block; -} -.diff-tailwindcss-wrapper .diff-widget-tooltip:hover::after { - display: block; -} -.diff-line-extend-wrapper, -.diff-line-widget-wrapper { - display: flow-root; -} -.diff-line-extend-wrapper * { - color: initial; -} -.diff-line-widget-wrapper * { - color: initial; -} -/* Multi-select styles for line range selection */ -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-new-num, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-old-num, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-num { - z-index: 2; -} -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-content, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-new-content, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-old-content { - position: relative; -} -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-new-num::after, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-old-num::after, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-num::after { - content: ""; - position: absolute; - z-index: 1; - inset: 0; - opacity: 0.15; - background-color: var(--diff-multi-select-bg, #f0c000); - pointer-events: none; -} -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-new-num::before, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-old-num::before, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-num::before { - content: ""; - z-index: 2; - position: absolute; - top: 0; - bottom: 0; - right: -2px; - width: 4px; - background-color: var(--diff-multi-select-border, #2588fa); -} -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-new-content::after, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-old-content::after, -.diff-tailwindcss-wrapper .diff-multi-select-active.diff-line-content::after { - content: ""; - position: absolute; - z-index: 1; - inset: 0; - opacity: 0.15; - background-color: var(--diff-multi-select-bg, #f0c000); - pointer-events: none; -} -/* Multi-select: ensure proper positioning for line number cells */ -.diff-multi-selecting .diff-line-old-num, -.diff-multi-selecting .diff-line-new-num, -.diff-multi-selecting .diff-line-num { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} -.diff-multi-selecting .diff-line-old-content, -.diff-multi-selecting .diff-line-new-content, -.diff-multi-selecting .diff-line-content { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} -/* Prevent text selection during multi-line selection */ -.diff-multi-selecting { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} -.diff-multi-selecting * { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} -/* Hide addWidget button during active dragging selection */ -.diff-multi-selecting .diff-add-widget-wrapper { - display: none; -} -/* Ensure addWidget button is above selection overlay */ -.diff-multiselect-wrapper .diff-add-widget-wrapper { - z-index: 10 !important; -} -/* Line number span should not interfere with click events */ -.diff-tailwindcss-wrapper .diff-line-new-num span[data-line-num], -.diff-tailwindcss-wrapper .diff-line-old-num span[data-line-num] { - pointer-events: none; -} -.diff-multiselect-wrapper .diff-line-old-num, -.diff-multiselect-wrapper .diff-line-new-num, -.diff-multiselect-wrapper .diff-line-num { - cursor: pointer; -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw pre code.hljs { - display: block; - overflow-x: auto; - padding: 1em -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw code.hljs { - padding: 3px 5px -} -/*! - Theme: GitHub - Description: Light theme as seen on github.com - Author: github.com - Maintainer: @Hirse - Updated: 2021-05-15 - - Outdated base version: https://github.com/primer/github-syntax-light - Current colors taken from GitHub's CSS -*/ -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs { - color: #24292e; - background: #ffffff -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-doctag, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-keyword, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-meta .hljs-keyword, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-template-tag, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-template-variable, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-type, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-variable.language_ { - /* prettylights-syntax-keyword */ - color: #d73a49 -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-title, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-title.class_, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-title.class_.inherited__, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-title.function_ { - /* prettylights-syntax-entity */ - color: #6f42c1 -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-attr, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-attribute, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-literal, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-meta, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-number, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-operator, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-variable, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-selector-attr, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-selector-class, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-selector-id { - /* prettylights-syntax-constant */ - color: #005cc5 -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-regexp, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-string, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-meta .hljs-string { - /* prettylights-syntax-string */ - color: #032f62 -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-built_in, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-symbol { - /* prettylights-syntax-variable */ - color: #e36209 -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-comment, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-code, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-formula { - /* prettylights-syntax-comment */ - color: #6a737d -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-name, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-quote, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-selector-tag, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-selector-pseudo { - /* prettylights-syntax-entity-tag */ - color: #22863a -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-subst { - /* prettylights-syntax-storage-modifier-import */ - color: #24292e -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-section { - /* prettylights-syntax-markup-heading */ - color: #005cc5; - font-weight: bold -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-bullet { - /* prettylights-syntax-markup-list */ - color: #735c0f -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-emphasis { - /* prettylights-syntax-markup-italic */ - color: #24292e; - font-style: italic -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-strong { - /* prettylights-syntax-markup-bold */ - color: #24292e; - font-weight: bold -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-addition { - /* prettylights-syntax-markup-inserted */ - color: #22863a; - background-color: #f0fff4 -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-deletion { - /* prettylights-syntax-markup-deleted */ - color: #b31d28; - background-color: #ffeef0 -} -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-char.escape_, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-link, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-params, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-property, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-punctuation, -.diff-tailwindcss-wrapper[data-theme="light"] .diff-line-syntax-raw .hljs-tag { - /* purposely ignored */ - -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw pre code.hljs { - display: block; - overflow-x: auto; - padding: 1em -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw code.hljs { - padding: 3px 5px -} -/*! - Theme: GitHub Dark - Description: Dark theme as seen on github.com - Author: github.com - Maintainer: @Hirse - Updated: 2021-05-15 - - Outdated base version: https://github.com/primer/github-syntax-dark - Current colors taken from GitHub's CSS -*/ -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs { - color: #c9d1d9; - background: #0d1117 -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-doctag, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-keyword, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-meta .hljs-keyword, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-template-tag, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-template-variable, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-type, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-variable.language_ { - /* prettylights-syntax-keyword */ - color: #ff7b72 -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-title, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-title.class_, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-title.class_.inherited__, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-title.function_ { - /* prettylights-syntax-entity */ - color: #d2a8ff -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-attr, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-attribute, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-literal, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-meta, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-number, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-operator, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-variable, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-selector-attr, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-selector-class, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-selector-id { - /* prettylights-syntax-constant */ - color: #79c0ff -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-regexp, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-string, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-meta .hljs-string { - /* prettylights-syntax-string */ - color: #a5d6ff -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-built_in, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-symbol { - /* prettylights-syntax-variable */ - color: #ffa657 -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-comment, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-code, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-formula { - /* prettylights-syntax-comment */ - color: #8b949e -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-name, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-quote, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-selector-tag, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-selector-pseudo { - /* prettylights-syntax-entity-tag */ - color: #7ee787 -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-subst { - /* prettylights-syntax-storage-modifier-import */ - color: #c9d1d9 -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-section { - /* prettylights-syntax-markup-heading */ - color: #1f6feb; - font-weight: bold -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-bullet { - /* prettylights-syntax-markup-list */ - color: #f2cc60 -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-emphasis { - /* prettylights-syntax-markup-italic */ - color: #c9d1d9; - font-style: italic -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-strong { - /* prettylights-syntax-markup-bold */ - color: #c9d1d9; - font-weight: bold -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-addition { - /* prettylights-syntax-markup-inserted */ - color: #aff5b4; - background-color: #033a16 -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-deletion { - /* prettylights-syntax-markup-deleted */ - color: #ffdcd7; - background-color: #67060c -} -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-char.escape_, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-link, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-params, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-property, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-punctuation, -.diff-tailwindcss-wrapper[data-theme="dark"] .diff-line-syntax-raw .hljs-tag { - /* purposely ignored */ - -} -.diff-tailwindcss-wrapper .hover\:scale-110:hover { - --tw-scale-x: 1.1; - --tw-scale-y: 1.1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.diff-tailwindcss-wrapper .group:hover .group-hover\:visible { - visibility: visible; -} diff --git a/public/vendor/diff-view-pure.css.br b/public/vendor/diff-view-pure.css.br deleted file mode 100644 index 04cbb92..0000000 Binary files a/public/vendor/diff-view-pure.css.br and /dev/null differ diff --git a/scripts/check-build-budgets.mjs b/scripts/check-build-budgets.mjs index 01d5434..1efba70 100644 --- a/scripts/check-build-budgets.mjs +++ b/scripts/check-build-budgets.mjs @@ -47,12 +47,13 @@ export const budgets = [ type: "loadable", }, { - // Guards the rich diff library (@git-diff-view/react), the heaviest deferred - // chunk. 340 KiB gzipped is intentionally generous because the library is - // large but always lazy-loaded. A jump here means a version bump pulled in - // more — confirm the upgrade is wanted before raising. - importKeyParts: ["components/renderers/diff-renderer", "@git-diff-view/react"], - maxBytes: 340 * 1024, + // Guards the deferred diff renderer chunk (@pierre/diffs core + renderer). + // Shiki grammars and themes are NOT in this chunk: the library loads them + // as separate on-demand chunks per language, outside this key. A jump here + // means a version bump grew the core — confirm the upgrade is wanted + // before raising. + importKeyParts: ["components/viewer/artifact-stage", "diff-renderer"], + maxBytes: 160 * 1024, name: "rich diff library deferred JS", type: "loadable", }, diff --git a/scripts/compress-dictionary.mjs b/scripts/compress-dictionary.mjs index 011aa04..4a301fb 100644 --- a/scripts/compress-dictionary.mjs +++ b/scripts/compress-dictionary.mjs @@ -8,15 +8,12 @@ * public/arx-dictionary.json.br — brotli-compressed (CDN or agent can use this) * public/arx2-dictionary.json — minified overlay JSON * public/arx2-dictionary.json.br — brotli-compressed overlay JSON - * public/vendor/diff-view-pure.css — mirrored @git-diff-view stylesheet - * public/vendor/diff-view-pure.css.br — brotli-compressed stylesheet loaded by diffs * * Run: node scripts/compress-dictionary.mjs */ -import { mkdirSync, readFileSync, writeFileSync } from "fs"; +import { readFileSync, writeFileSync } from "fs"; import { createRequire } from "module"; -import { dirname } from "path"; const require = createRequire(import.meta.url); const brotli = require("brotli-wasm"); @@ -39,17 +36,5 @@ function compressDictionary(path) { writeCompressedTextAsset(path, minified); } -function mirrorAndCompressTextAsset(sourcePath, targetPath) { - const source = readFileSync(sourcePath, "utf8"); - - mkdirSync(dirname(targetPath), { recursive: true }); - writeFileSync(targetPath, source, "utf8"); - writeCompressedTextAsset(targetPath, source); -} - compressDictionary("public/arx-dictionary.json"); compressDictionary("public/arx2-dictionary.json"); -mirrorAndCompressTextAsset( - "node_modules/@git-diff-view/react/styles/diff-view-pure.css", - "public/vendor/diff-view-pure.css", -); diff --git a/src/app/globals.css b/src/app/globals.css index 7da3c64..3eaa9c1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1713,9 +1713,6 @@ select.creator-input { margin-left: -1px; } -.diff-mobile-note { - min-height: 2.1rem; -} /* Diff bodies: dark charcoal in BOTH themes (also supplies the vendor palette) */ .diff-renderer-frame { @@ -1725,36 +1722,46 @@ select.creator-input { background: #1c1915; } -.diff-tailwindcss-wrapper, -.diff-renderer-frame [data-theme="light"], -.diff-renderer-frame [data-theme="dark"] { +/* @pierre/diffs renders in shadow DOM; these custom properties inherit through + the shadow boundary and pin the chrome to the charcoal chassis. Syntax tokens + come from the pinned pierre-dark Shiki theme. */ +.diff-renderer-frame { color-scheme: dark; color: #e6dfcf; - --diff-bgColor-default: #1c1915; - --diff-add-content--: #1e2f24; - --diff-del-content--: #3a201b; - --diff-add-content-highlight--: #2c4a38; - --diff-del-content-highlight--: #57271f; - --diff-border--: #cfc6b41f; - --diff-add-lineNumber--: #22352b; - --diff-del-lineNumber--: #41231e; - --diff-plain-content--: #1c1915; - --diff-expand-content--: #181510; - --diff-plain-lineNumber-color--: #8b8271; - --diff-expand-lineNumber-color--: #6e6656; - --diff-plain-lineNumber--: #181510; - --diff-expand-lineNumber--: #14110d; - --diff-hunk-content--: #26221b; - --diff-hunk-content-color--: #b0a794; - --diff-hunk-lineNumber--: #26221b; - --diff-add-widget--: #26221b; - --diff-add-widget-color--: #b0a794; - --diff-empty-content--: #181510; -} - -.diff-tailwindcss-wrapper { - font-family: var(--font-mono), monospace; - font-size: 0.74rem; + --diffs-font-family: var(--font-mono), monospace; + --diffs-header-font-family: var(--font-mono), monospace; + --diffs-font-size: 0.74rem; + --diffs-bg: #1c1915; + --diffs-fg: #e6dfcf; + /* Tokens for the "agent-render" CSS-variables Shiki theme (registered in + src/lib/diff/pierre-react.ts): the app's rainbow palette, so diff syntax + matches the code renderer instead of a foreign theme. */ + --diffs-foreground: #e6dfcf; + --diffs-background: #1c1915; + --diffs-token-keyword: var(--rb-0); + --diffs-token-function: var(--rb-4); + --diffs-token-string: var(--rb-3); + --diffs-token-string-expression: var(--rb-3); + --diffs-token-constant: var(--rb-1); + --diffs-token-parameter: var(--rb-2); + --diffs-token-link: var(--rb-2); + --diffs-token-comment: #8b8271; + --diffs-token-punctuation: #b0a794; + --diffs-token-inserted: #9ccfae; + --diffs-token-deleted: #d96a5c; + --diffs-token-changed: var(--rb-1); + --diffs-bg-context-override: #26221b; + --diffs-bg-separator-override: #181510; + --diffs-bg-hover-override: #26221b; + --diffs-fg-number-override: #8b8271; + --diffs-addition-color-override: #9ccfae; + --diffs-deletion-color-override: #d96a5c; + --diffs-bg-addition-override: #1e2f24; + --diffs-bg-addition-number-override: #22352b; + --diffs-bg-addition-emphasis-override: #2c4a38; + --diffs-bg-deletion-override: #3a201b; + --diffs-bg-deletion-number-override: #41231e; + --diffs-bg-deletion-emphasis-override: #57271f; } .patch-bundle-shell { @@ -2332,10 +2339,6 @@ select.creator-input { gap: 0.45rem; } - .diff-mobile-note { - justify-content: flex-start; - min-height: 0; - } .diff-renderer-shell[data-mobile-layout="true"] .diff-view-toggle .artifact-action { flex: 0 1 auto; diff --git a/src/components/renderers/diff-renderer.tsx b/src/components/renderers/diff-renderer.tsx index 7235ffe..2d96ead 100644 --- a/src/components/renderers/diff-renderer.tsx +++ b/src/components/renderers/diff-renderer.tsx @@ -2,16 +2,11 @@ import { Component, type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { Check, Columns2, Copy, Rows3 } from "lucide-react"; -import { useResolvedTheme, type ResolvedTheme } from "@/components/theme/use-theme-controller"; +import { PatchDiff, MultiFileDiff, type FileDiffProps } from "@/lib/diff/pierre-react"; import { copyTextToClipboard } from "@/lib/copy-text"; import { detectCodeLanguage } from "@/lib/code/language"; import { parseGitPatchBundle } from "@/lib/diff/git-patch"; import type { DiffArtifact } from "@/lib/payload/schema"; -import { - loadDiffViewStylesheet, - releaseDiffViewStylesheet, - retainDiffViewStylesheet, -} from "@/components/renderers/diff-view-stylesheet"; type DiffRendererProps = { artifact: DiffArtifact; @@ -21,23 +16,22 @@ type DiffRendererProps = { const NARROW_DIFF_BREAKPOINT = 640; const MOBILE_DIFF_MEDIA_QUERY = `(max-width: ${NARROW_DIFF_BREAKPOINT}px)`; -type DiffViewModule = typeof import("@git-diff-view/react"); -type DiffViewLibrary = Pick; type DiffViewMode = "unified" | "split"; -type DiffFileInstance = InstanceType; +type DiffOptions = NonNullable["options"]>; -type RenderableDiffFile = { +type RenderablePatchFile = { meta: ReturnType[number]; - diffFile: DiffFileInstance | null; }; type DiffRenderState = | { - kind: "loading"; + kind: "rich-patch"; + patchFiles: RenderablePatchFile[]; } | { - kind: "rich"; - diffFiles: RenderableDiffFile[]; + kind: "rich-contents"; + fileName: string; + language: string | undefined; } | { kind: "fallback"; @@ -46,22 +40,6 @@ type DiffRenderState = detail?: string; }; -type ParsedPatchBundleState = - | { - kind: "none"; - } - | { - kind: "invalid-shape"; - } - | { - error: unknown; - kind: "parse-error"; - } - | { - kind: "parsed"; - patchFiles: ReturnType; - }; - type DiffRendererBoundaryProps = { artifact: DiffArtifact; onReady?: () => void; @@ -95,36 +73,18 @@ function getDefaultMode(view: DiffArtifact["view"], isNarrowScreen: boolean) { return view === "split" && !isNarrowScreen ? "split" : "unified"; } -function getDiffLibraryMode(mode: DiffViewMode, diffLibrary: DiffViewLibrary) { - return mode === "split" ? diffLibrary.DiffModeEnum.Split : diffLibrary.DiffModeEnum.Unified; -} - -function patchFilesNeedDiffLibrary(patchFiles: ReturnType): boolean { - for (const patchFile of patchFiles) { - if (!patchFile.isBinary) { - return true; - } - } - - return false; -} - -function diffFilesHaveRenderableFile(diffFiles: RenderableDiffFile[]): boolean { - for (const { diffFile } of diffFiles) { - if (diffFile) { - return true; - } - } - - return false; -} - -/** Run the four-call @git-diff-view warmup (theme + parse both view modes) a DiffFile needs before render. */ -function warmDiffFile(diffFile: DiffFileInstance, resolvedTheme: ResolvedTheme): void { - diffFile.initTheme(resolvedTheme === "dark" ? "dark" : "light"); - diffFile.init(); - diffFile.buildSplitDiffLines(); - diffFile.buildUnifiedDiffLines(); +// The diff bodies stay on the dark charcoal chassis in BOTH app themes (design +// contract), so the Shiki theme is pinned dark; chrome colors come from the +// --diffs-* custom properties set in globals.css, which pierce the shadow DOM. +function getDiffOptions(mode: DiffViewMode): DiffOptions { + return { + diffStyle: mode, + theme: "agent-render", + themeType: "dark", + overflow: "wrap", + disableFileHeader: true, + diffIndicators: "classic", + }; } function looksLikeUnifiedDiff(patch: string) { @@ -156,38 +116,6 @@ function getFallbackState(artifact: DiffArtifact, message: string, error?: unkno }; } -function buildRenderablePatchFile( - patchFile: ReturnType[number], - artifact: DiffArtifact, - resolvedTheme: ResolvedTheme, - diffLibrary: DiffViewLibrary, -): RenderableDiffFile { - if (patchFile.isBinary) { - return { - meta: patchFile, - diffFile: null, - }; - } - - const language = detectCodeLanguage(patchFile.newPath ?? patchFile.oldPath ?? undefined, artifact.language); - const diffFile = new diffLibrary.DiffFile( - patchFile.oldPath ? `a/${patchFile.oldPath}` : "/dev/null", - "", - patchFile.newPath ? `b/${patchFile.newPath}` : "/dev/null", - "", - [patchFile.patch], - language, - language, - ); - - warmDiffFile(diffFile, resolvedTheme); - - return { - meta: patchFile, - diffFile, - }; -} - const diffFallbackFrameStyle = { overflow: "auto", border: "1px solid var(--border)", @@ -294,10 +222,6 @@ function DiffFallback({ data-mobile-layout={getIsNarrowScreen() ? "true" : "false"} >
-
- raw patch fallback - invalid unified diff -
{rawPatch ? ( - ))} - -
- {diffFiles.map(({ meta, diffFile }) => ( -
+ renderedDiff.kind === "rich-contents" ? ( +
+
+
-

{meta.status}

-

{meta.displayPath}

+

modified

+

{renderedDiff.fileName}

- {meta.oldPath && meta.newPath && meta.oldPath !== meta.newPath ? ( - {meta.oldPath} -> {meta.newPath} - ) : null}
- {meta.isBinary || !diffFile ? ( -
Binary patch preview is not expanded. Download the patch to inspect the raw binary diff headers.
- ) : RichDiffView && richDiffMode ? ( - - ) : null} +
- ))} +
-
+ ) : ( +
+ +
+ {renderedDiff.patchFiles.map(({ meta }) => ( +
+
+
+

{meta.status}

+

{meta.displayPath}

+
+ {meta.oldPath && meta.newPath && meta.oldPath !== meta.newPath ? ( + {meta.oldPath} -> {meta.newPath} + ) : null} +
+ {meta.isBinary ? ( +
Binary patch preview is not expanded. Download the patch to inspect the raw binary diff headers.
+ ) : ( + + )} +
+ ))} +
+
+ ) ) : null}
@@ -714,6 +467,7 @@ function DiffRendererContent({ artifact, onReady }: DiffRendererProps) { * Renders diff artifacts as review-style unified/split views in the artifact stage. * Uses `artifact` diff payload details and optional `onReady` callback when the active diff UI is mount-ready. * Prefers parsed git patches, supports old/new content diffs, and falls back to raw patch output on parse/runtime errors. + * Rendering is delegated to @pierre/diffs (Shiki-based, shadow DOM); the diff bodies stay dark in both app themes. */ export function DiffRenderer({ artifact, onReady }: DiffRendererProps) { // resetKey hashes patch/content (FNV-1a + length) as a deliberate bound to avoid embedding huge diff --git a/src/components/renderers/diff-view-stylesheet.ts b/src/components/renderers/diff-view-stylesheet.ts deleted file mode 100644 index 97f85d4..0000000 --- a/src/components/renderers/diff-view-stylesheet.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { withBasePath } from "@/lib/site/base-path"; - -// Ref-counted loader for the @git-diff-view vendor stylesheet. The diff renderer is the only consumer -// and mounts at most a few instances, so a module-level refcount keeps the heavy stylesheet injected -// exactly while a rich diff is on screen and removes it once the last instance unmounts. - -const DIFF_VIEW_STYLESHEET_ID = "agent-render-diff-view-styles"; -const diffViewStylesheetHrefs = [ - withBasePath("/vendor/diff-view-pure.css.br"), - withBasePath("/vendor/diff-view-pure.css"), -]; - -let diffViewStylesheetPromise: Promise | null = null; -let diffViewStylesheetRefCount = 0; - -function loadStylesheetHref(href: string) { - return new Promise((resolve, reject) => { - const link = document.createElement("link"); - - const cleanup = () => { - link.removeEventListener("load", handleLoad); - link.removeEventListener("error", handleError); - }; - const handleLoad = () => { - link.dataset.loaded = "true"; - cleanup(); - resolve(); - }; - const handleError = () => { - cleanup(); - link.remove(); - reject(new Error(`Diff view stylesheet failed to load: ${href}`)); - }; - - link.id = DIFF_VIEW_STYLESHEET_ID; - link.rel = "stylesheet"; - link.href = href; - link.addEventListener("load", handleLoad); - link.addEventListener("error", handleError); - document.head.appendChild(link); - }); -} - -/** Inject the diff-view stylesheet (preferring the precompressed variant), de-duplicating in flight. */ -export function loadDiffViewStylesheet() { - if (typeof document === "undefined") { - return Promise.resolve(); - } - - const existingLink = document.getElementById(DIFF_VIEW_STYLESHEET_ID) as HTMLLinkElement | null; - - if (existingLink?.dataset.loaded === "true" || existingLink?.sheet) { - if (existingLink) { - existingLink.dataset.loaded = "true"; - } - return Promise.resolve(); - } - - if (diffViewStylesheetPromise && !existingLink) { - diffViewStylesheetPromise = null; - } - - if (diffViewStylesheetPromise) { - return diffViewStylesheetPromise; - } - - existingLink?.remove(); - - diffViewStylesheetPromise = (async () => { - let lastError: unknown; - - for (const href of diffViewStylesheetHrefs) { - try { - await loadStylesheetHref(href); - return; - } catch (error) { - lastError = error; - } - } - - throw lastError instanceof Error ? lastError : new Error("Diff view stylesheet failed to load."); - })().catch((error) => { - diffViewStylesheetPromise = null; - throw error; - }); - - return diffViewStylesheetPromise; -} - -/** Mark one diff instance as using the stylesheet. */ -export function retainDiffViewStylesheet() { - diffViewStylesheetRefCount += 1; -} - -/** Release one diff instance; removes the stylesheet once the last consumer unmounts. */ -export function releaseDiffViewStylesheet() { - diffViewStylesheetRefCount = Math.max(0, diffViewStylesheetRefCount - 1); - if (diffViewStylesheetRefCount > 0) { - return; - } - - document.getElementById(DIFF_VIEW_STYLESHEET_ID)?.remove(); - diffViewStylesheetPromise = null; -} diff --git a/src/lib/diff/pierre-react.ts b/src/lib/diff/pierre-react.ts new file mode 100644 index 0000000..26ace12 --- /dev/null +++ b/src/lib/diff/pierre-react.ts @@ -0,0 +1,30 @@ +"use client"; + +import { registerCustomCSSVariableTheme } from "@pierre/diffs"; + +// Re-export pierre react primitives so the deferred diff-renderer chunk stays +// stable across Next webpack dev/prod graphs (a direct deep import from the +// dynamic chunk produced "__webpack_modules__[moduleId] is not a function" +// chunk-id drift in a prior attempt). Also the single seam unit tests mock. +export { PatchDiff, MultiFileDiff, type FileDiffProps } from "@pierre/diffs/react"; + +// Shiki theme driven by --diffs-token-* custom properties, which inherit +// through the shadow boundary from .diff-renderer-frame (globals.css) and tie +// diff syntax to the app's own rainbow palette instead of a foreign theme. +// The values here are baked-in fallbacks for contexts without the app CSS. +registerCustomCSSVariableTheme("agent-render", { + foreground: "#e6dfcf", + background: "#1c1915", + "token-keyword": "#f08d5e", + "token-function": "#9eb3ff", + "token-string": "#80c193", + "token-string-expression": "#80c193", + "token-constant": "#efb360", + "token-parameter": "#69d1dd", + "token-link": "#69d1dd", + "token-comment": "#8b8271", + "token-punctuation": "#b0a794", + "token-inserted": "#9ccfae", + "token-deleted": "#d96a5c", + "token-changed": "#efb360", +}); diff --git a/tests/build-budgets.test.ts b/tests/build-budgets.test.ts index d272d94..338036a 100644 --- a/tests/build-budgets.test.ts +++ b/tests/build-budgets.test.ts @@ -15,7 +15,7 @@ describe("build budget policy", () => { { name: "homepage route JS", maxBytes: 115 * 1024 }, { name: "code renderer deferred JS", maxBytes: 100 * 1024 }, { name: "markdown renderer deferred JS", maxBytes: 52 * 1024 }, - { name: "rich diff library deferred JS", maxBytes: 340 * 1024 }, + { name: "rich diff library deferred JS", maxBytes: 160 * 1024 }, ]); }); }); diff --git a/tests/components/diff-renderer.test.tsx b/tests/components/diff-renderer.test.tsx index 6ba3f19..57aa587 100644 --- a/tests/components/diff-renderer.test.tsx +++ b/tests/components/diff-renderer.test.tsx @@ -2,18 +2,22 @@ import React from "react"; import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import { DiffFile } from "@git-diff-view/react"; import { DiffRenderer } from "@/components/renderers/diff-renderer"; import type { DiffArtifact } from "@/lib/payload/schema"; -vi.mock("@git-diff-view/react", async () => { - const actual = await vi.importActual("@git-diff-view/react"); +const patchDiffMock = vi.fn(); +const multiFileDiffMock = vi.fn(); - return { - ...actual, - DiffView: () =>
Rich diff view
, - }; -}); +vi.mock("@/lib/diff/pierre-react", () => ({ + PatchDiff: (props: { patch: string }) => { + patchDiffMock(props); + return
Rich patch diff
; + }, + MultiFileDiff: (props: { oldFile: { contents: string }; newFile: { contents: string } }) => { + multiFileDiffMock(props); + return
Rich contents diff
; + }, +})); const validPatch = `diff --git a/src/hello.ts b/src/hello.ts index 1111111..2222222 100644 @@ -93,7 +97,8 @@ afterAll(() => { afterEach(() => { cleanup(); - document.getElementById("agent-render-diff-view-styles")?.remove(); + patchDiffMock.mockClear(); + multiFileDiffMock.mockClear(); vi.restoreAllMocks(); }); @@ -106,17 +111,31 @@ describe("DiffRenderer", () => { }); expect(screen.queryByText(/could not be rendered as a valid unified diff/i)).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: /src\/hello\.ts/i })).toBeVisible(); - expect(screen.getByTestId("mock-rich-diff-view")).toBeVisible(); + expect(screen.getByTestId("mock-patch-diff")).toBeVisible(); + expect(patchDiffMock).toHaveBeenCalledWith(expect.objectContaining({ patch: expect.stringContaining("diff --git") })); }); - it("loads the diff-view stylesheet only from the deferred public asset", async () => { - render(); + it("renders before-and-after content diffs through the contents path", async () => { + render( + , + ); await waitFor(() => { - const stylesheet = document.getElementById("agent-render-diff-view-styles"); - expect(stylesheet).toBeInstanceOf(HTMLLinkElement); - expect((stylesheet as HTMLLinkElement).href).toContain("/vendor/diff-view-pure.css.br"); + expect(screen.getByTestId("renderer-diff")).toHaveAttribute("data-diff-state", "rich"); }); + expect(screen.getByTestId("mock-multi-file-diff")).toBeVisible(); + expect(multiFileDiffMock).toHaveBeenCalledWith( + expect.objectContaining({ + oldFile: expect.objectContaining({ contents: expect.stringContaining("old") }), + newFile: expect.objectContaining({ contents: expect.stringContaining("new") }), + }), + ); }); it("falls back to the raw patch when the diff parser rejects malformed hunks", async () => { @@ -155,25 +174,10 @@ describe("DiffRenderer", () => { expect(secondReady).not.toHaveBeenCalled(); }); - it("removes the rich diff stylesheet when switching to the fallback path", async () => { - const { rerender } = render(); - - await waitFor(() => { - expect(document.getElementById("agent-render-diff-view-styles")).toBeInstanceOf(HTMLLinkElement); - }); - - rerender(); - - await waitFor(() => { - expect(screen.getByTestId("renderer-diff")).toHaveAttribute("data-diff-state", "fallback"); - }); - expect(document.getElementById("agent-render-diff-view-styles")).not.toBeInTheDocument(); - }); - - it("falls back to the raw patch when the diff library throws during parsing", async () => { + it("falls back to the raw patch when the rich diff component throws at render", async () => { const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); - vi.spyOn(DiffFile.prototype, "init").mockImplementation(() => { - throw new Error("Invalid hunk header format"); + patchDiffMock.mockImplementation(() => { + throw new Error("shadow root exploded"); }); try { @@ -183,33 +187,29 @@ describe("DiffRenderer", () => { expect(screen.getByTestId("renderer-diff")).toHaveAttribute("data-diff-state", "fallback"); }); expect(screen.getByText(/could not be rendered as a valid unified diff/i)).toBeVisible(); - expect(screen.getByText(/parser detail: Invalid hunk header format/i)).toBeVisible(); + expect(screen.getByText(/parser detail: shadow root exploded/i)).toBeVisible(); expect(screen.getByTestId("renderer-diff-fallback-raw")).toHaveTextContent('export const hello = "new";'); } finally { consoleError.mockRestore(); } }); - it("skips diff parsing for binary patches and keeps the rich renderer shell", async () => { - const initSpy = vi.spyOn(DiffFile.prototype, "init"); - + it("skips diff rendering for binary patches and keeps the rich renderer shell", async () => { render(); const renderer = await screen.findByTestId("renderer-diff"); expect(renderer).toHaveAttribute("data-diff-state", "rich"); - expect(initSpy).not.toHaveBeenCalled(); + expect(patchDiffMock).not.toHaveBeenCalled(); expect(screen.getByText(/binary patch preview is not expanded/i)).toBeVisible(); expect(screen.queryByText(/could not be rendered as a valid unified diff/i)).not.toBeInTheDocument(); }); it("keeps the rich/binary path for a CRLF binary patch instead of the raw fallback", async () => { - const initSpy = vi.spyOn(DiffFile.prototype, "init"); - render(); const renderer = await screen.findByTestId("renderer-diff"); expect(renderer).toHaveAttribute("data-diff-state", "rich"); - expect(initSpy).not.toHaveBeenCalled(); + expect(patchDiffMock).not.toHaveBeenCalled(); expect(screen.getByText(/binary patch preview is not expanded/i)).toBeVisible(); expect(screen.queryByTestId("renderer-diff-fallback-raw")).not.toBeInTheDocument(); expect(screen.queryByText(/not a valid unified diff/i)).not.toBeInTheDocument(); diff --git a/tests/diff-style-asset.test.ts b/tests/diff-style-asset.test.ts deleted file mode 100644 index e14e111..0000000 --- a/tests/diff-style-asset.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { brotliDecompressSync } from "node:zlib"; -import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { describe, expect, it } from "vitest"; - -describe("diff stylesheet asset", () => { - it("keeps the deferred public stylesheet in sync with the diff-view package", () => { - const packageStylesheet = readFileSync( - resolve("node_modules/@git-diff-view/react/styles/diff-view-pure.css"), - "utf-8", - ); - const publicStylesheet = readFileSync( - resolve("public/vendor/diff-view-pure.css"), - "utf-8", - ); - - expect(publicStylesheet).toBe(packageStylesheet); - }); - - it("keeps the precompressed deferred stylesheet in sync with the plain asset", () => { - const publicStylesheet = readFileSync( - resolve("public/vendor/diff-view-pure.css"), - "utf-8", - ); - const compressedStylesheet = readFileSync( - resolve("public/vendor/diff-view-pure.css.br"), - ); - - expect(brotliDecompressSync(compressedStylesheet).toString("utf-8")).toBe(publicStylesheet); - }); -}); diff --git a/tests/e2e/viewer.spec.ts b/tests/e2e/viewer.spec.ts index 40b8f18..4bd2870 100644 --- a/tests/e2e/viewer.spec.ts +++ b/tests/e2e/viewer.spec.ts @@ -158,23 +158,22 @@ test("renders multi-file diffs without mutating the payload hash", async ({ page await expect.poll(() => page.evaluate(() => window.location.hash)).toBe(beforeHash); }); -test("loads the compressed diff stylesheet only after opening a diff artifact", async ({ page }) => { - await waitForViewerState(page, "empty"); - await expect - .poll(() => page.evaluate(() => Array.from(document.querySelectorAll('link[rel="stylesheet"]')).some((link) => link.getAttribute("href")?.includes("diff-view-pure.css")))) - .toBe(false); - +test("renders rich diffs in shadow DOM without any external diff stylesheet", async ({ page }) => { await goToHash(page, getFragmentHash("Phase 1 sample diff")); await waitForViewerState(page, "artifact"); await waitForRendererReady(page, "diff"); await expect(page.getByTestId("renderer-diff")).toHaveAttribute("data-diff-state", "rich"); + // @pierre/diffs renders into shadow roots; styling ships inside them. + await expect + .poll(() => page.evaluate(() => Array.from(document.querySelectorAll(".patch-file-section *")).some((element) => element.shadowRoot !== null))) + .toBe(true); + const stylesheetHrefs = await page.evaluate(() => Array.from(document.querySelectorAll('link[rel="stylesheet"]'), (link) => link.getAttribute("href") ?? "")); - expect(stylesheetHrefs.some((href) => href.endsWith("/vendor/diff-view-pure.css.br"))).toBe(true); - expect(stylesheetHrefs.some((href) => href.endsWith("/vendor/diff-view-pure.css"))).toBe(false); + expect(stylesheetHrefs.some((href) => href.includes("diff-view"))).toBe(false); }); -test("keeps fallback diffs off the rich diff stylesheet path", async ({ page }) => { +test("shows the raw patch fallback for invalid unified diffs", async ({ page }) => { const fallbackDiffEnvelope = { v: 1, codec: "plain", @@ -195,9 +194,6 @@ test("keeps fallback diffs off the rich diff stylesheet path", async ({ page }) await expect(page.getByTestId("renderer-diff")).toHaveAttribute("data-diff-state", "fallback"); await expect(page.locator('[data-testid="viewer-shell"][data-renderer-ready="true"]')).toBeVisible(); await expect(page.getByTestId("renderer-diff-fallback-raw")).toContainText("not a unified diff"); - await expect - .poll(() => page.evaluate(() => Array.from(document.querySelectorAll('link[rel="stylesheet"]')).some((link) => link.getAttribute("href")?.includes("diff-view-pure.css")))) - .toBe(false); }); test.describe("mobile UX", () => { diff --git a/tests/headers.test.ts b/tests/headers.test.ts index 79590e9..a4a9278 100644 --- a/tests/headers.test.ts +++ b/tests/headers.test.ts @@ -31,9 +31,7 @@ describe("static security headers", () => { expect(headers).toContain("/arx-dictionary.json.br"); expect(headers).toContain("/arx2-dictionary.json.br"); - expect(headers).toContain("/vendor/diff-view-pure.css.br"); expect(headers).toContain("Content-Type: application/json; charset=utf-8"); - expect(headers).toContain("Content-Type: text/css; charset=utf-8"); expect(headers).toContain("Content-Encoding: br"); expect(headers).toContain("Vary: Accept-Encoding"); }); diff --git a/tests/selfhosted/api-catalog.test.ts b/tests/selfhosted/api-catalog.test.ts index 901454f..6943134 100644 --- a/tests/selfhosted/api-catalog.test.ts +++ b/tests/selfhosted/api-catalog.test.ts @@ -53,7 +53,7 @@ function createExportFixture(): { root: string; outDir: string } { "brotli-compressed-json", ); writeFileSync( - path.join(outDir, "vendor", "diff-view-pure.css.br"), + path.join(outDir, "vendor", "fixture-style.css.br"), "brotli-compressed-css", ); writeFileSync( @@ -243,7 +243,7 @@ describe("RFC 9727 api-catalog", () => { ); const compressedDiffStyleResponse = await fetch( - `http://127.0.0.1:${port}/vendor/diff-view-pure.css.br`, + `http://127.0.0.1:${port}/vendor/fixture-style.css.br`, { method: "HEAD" }, ); expect(compressedDiffStyleResponse.status).toBe(200); @@ -372,7 +372,7 @@ describe("RFC 9727 api-catalog", () => { ); const compressedDiffStyleResponse = await fetch( - `http://127.0.0.1:${port}/vendor/diff-view-pure.css.br`, + `http://127.0.0.1:${port}/vendor/fixture-style.css.br`, { method: "HEAD" }, ); expect(compressedDiffStyleResponse.status).toBe(200); diff --git a/tests/selfhosted/static-headers.test.ts b/tests/selfhosted/static-headers.test.ts index 34a7fd1..adb7f21 100644 --- a/tests/selfhosted/static-headers.test.ts +++ b/tests/selfhosted/static-headers.test.ts @@ -51,7 +51,7 @@ function createExportFixture(): { root: string; outDir: string } { "brotli-compressed-json", ); writeFileSync( - path.join(outDir, "vendor", "diff-view-pure.css.br"), + path.join(outDir, "vendor", "fixture-style.css.br"), "brotli-compressed-css", ); return { root, outDir }; @@ -140,7 +140,7 @@ describe("selfhosted precompressed header contract", () => { it("serves *.css.br with decompressed Content-Type and Brotli headers", async () => { const response = await fetch( - `http://127.0.0.1:${port}/vendor/diff-view-pure.css.br`, + `http://127.0.0.1:${port}/vendor/fixture-style.css.br`, { method: "HEAD" }, ); expect(response.status).toBe(200); diff --git a/tests/serve-export-headers.test.ts b/tests/serve-export-headers.test.ts index 0bcba4c..ade77d8 100644 --- a/tests/serve-export-headers.test.ts +++ b/tests/serve-export-headers.test.ts @@ -51,7 +51,7 @@ function createExportFixture(): { root: string } { "brotli-compressed-json", ); writeFileSync( - path.join(outDir, "vendor", "diff-view-pure.css.br"), + path.join(outDir, "vendor", "fixture-style.css.br"), "brotli-compressed-css", ); return { root }; @@ -134,7 +134,7 @@ describe("serve-export precompressed header contract", () => { it("serves *.css.br with decompressed Content-Type and Brotli headers", async () => { const response = await fetch( - `http://127.0.0.1:${port}/vendor/diff-view-pure.css.br`, + `http://127.0.0.1:${port}/vendor/fixture-style.css.br`, { method: "HEAD" }, ); expect(response.status).toBe(200);