From 2baa4692f71d4ccb0ea018663205094483ce1b63 Mon Sep 17 00:00:00 2001 From: fadeltd Date: Fri, 30 Oct 2026 09:17:49 +0700 Subject: [PATCH 1/6] Follow-ups scaffold: shared CodeMirror theme, @codemirror/lang-json Moves the editor's base theme out of the Diff route into src/lib/codemirror so the JSON tool can use it, and pins @codemirror/lang-json 6.0.2 and @codemirror/language 6.12.4 (already in the lockfile) for the JSON output's fold gutter. --- package.json | 2 + pnpm-lock.yaml | 23 ++++++++++ src/lib/codemirror/theme.ts | 41 ++++++++++++++++++ src/tools/diff/theme.ts | 85 +++++++++++++------------------------ 4 files changed, 95 insertions(+), 56 deletions(-) create mode 100644 src/lib/codemirror/theme.ts diff --git a/package.json b/package.json index 71b7868..134f787 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "core:build": "pnpm --filter @fadeltd/devtools-core build" }, "dependencies": { + "@codemirror/lang-json": "6.0.2", + "@codemirror/language": "6.12.4", "@codemirror/merge": "6.12.2", "@codemirror/state": "6.7.5", "@codemirror/view": "6.43.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03f6ba4..bd904d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@codemirror/lang-json': + specifier: 6.0.2 + version: 6.0.2 + '@codemirror/language': + specifier: 6.12.4 + version: 6.12.4 '@codemirror/merge': specifier: 6.12.2 version: 6.12.2 @@ -710,6 +716,9 @@ packages: '@codemirror/commands@6.11.1': resolution: {integrity: sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==} + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + '@codemirror/language@6.12.4': resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} @@ -1085,6 +1094,9 @@ packages: '@lezer/highlight@1.2.3': resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + '@lezer/lr@1.4.10': resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} @@ -4168,6 +4180,11 @@ snapshots: '@codemirror/view': 6.43.12 '@lezer/common': 1.5.2 + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + '@codemirror/language@6.12.4': dependencies: '@codemirror/state': 6.7.5 @@ -4438,6 +4455,12 @@ snapshots: dependencies: '@lezer/common': 1.5.2 + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/lr@1.4.10': dependencies: '@lezer/common': 1.5.2 diff --git a/src/lib/codemirror/theme.ts b/src/lib/codemirror/theme.ts new file mode 100644 index 0000000..57b0f6c --- /dev/null +++ b/src/lib/codemirror/theme.ts @@ -0,0 +1,41 @@ +import { EditorView } from '@codemirror/view' + +/** + * Matches the app's design tokens so an editor does not look bolted on: + * 44px right-aligned line-number gutter on its own darker surface, and + * 12px/18px tabular mono. Shared by the Diff and JSON tools; each adds its + * own rules on top. + */ +export const editorTheme = EditorView.theme( + { + '&': { + backgroundColor: 'var(--color-bg)', + color: 'var(--color-fg)', + height: '100%', + fontSize: '12px', + }, + '.cm-scroller': { + fontFamily: 'var(--font-mono)', + fontVariantNumeric: 'tabular-nums', + lineHeight: '18px', + }, + '.cm-gutters': { + backgroundColor: 'var(--color-gutter)', + color: 'var(--color-faint)', + border: 'none', + borderRight: '1px solid var(--color-border)', + userSelect: 'none', + }, + '.cm-lineNumbers .cm-gutterElement': { + minWidth: '44px', + padding: '0 6px 0 0', + textAlign: 'right', + }, + '.cm-activeLine': { backgroundColor: 'rgb(255 255 255 / 0.03)' }, + '.cm-activeLineGutter': { backgroundColor: 'transparent', color: 'var(--color-muted)' }, + '.cm-content': { caretColor: 'var(--color-accent)' }, + '&.cm-focused': { outline: 'none' }, + '.cm-selectionBackground, ::selection': { backgroundColor: 'var(--color-accent-dim)' }, + }, + { dark: true }, +) diff --git a/src/tools/diff/theme.ts b/src/tools/diff/theme.ts index 903014e..7ac79a5 100644 --- a/src/tools/diff/theme.ts +++ b/src/tools/diff/theme.ts @@ -1,63 +1,36 @@ import { EditorView } from '@codemirror/view' +import { editorTheme } from '@/lib/codemirror/theme' /** - * Matches the app's design tokens so the editor does not look bolted on. - * Follows the design system's diff spec: 44px right-aligned line-number gutter - * on its own darker surface, 12px/18px tabular mono, and the semantic add/del - * colours at 12% background tint with full-strength foreground. + * The shared editor theme plus the design system's diff spec: the semantic + * add/del colours at 12% background tint with full-strength foreground. */ -export const diffTheme = EditorView.theme( - { - '&': { - backgroundColor: 'var(--color-bg)', - color: 'var(--color-fg)', - height: '100%', - fontSize: '12px', - }, - '.cm-scroller': { - fontFamily: 'var(--font-mono)', - fontVariantNumeric: 'tabular-nums', - lineHeight: '18px', - }, - '.cm-gutters': { - backgroundColor: 'var(--color-gutter)', - color: 'var(--color-faint)', - border: 'none', - borderRight: '1px solid var(--color-border)', - userSelect: 'none', - }, - '.cm-lineNumbers .cm-gutterElement': { - minWidth: '44px', - padding: '0 6px 0 0', - textAlign: 'right', - }, - '.cm-activeLine': { backgroundColor: 'rgb(255 255 255 / 0.03)' }, - '.cm-activeLineGutter': { backgroundColor: 'transparent', color: 'var(--color-muted)' }, - '.cm-content': { caretColor: 'var(--color-accent)' }, - '&.cm-focused': { outline: 'none' }, - '.cm-selectionBackground, ::selection': { backgroundColor: 'var(--color-accent-dim)' }, +export const diffTheme = [ + editorTheme, + EditorView.theme( + { + '.cm-changedLine': { backgroundColor: 'var(--color-warn-bg)' }, + '.cm-changedText': { backgroundColor: 'rgb(245 158 11 / 0.25)' }, + '.cm-insertedLine': { backgroundColor: 'var(--color-add-bg)' }, + '.cm-deletedLine': { backgroundColor: 'var(--color-del-bg)' }, + '.cm-deletedChunk': { backgroundColor: 'var(--color-del-bg)' }, + '.cm-deletedChunk .cm-deletedText': { backgroundColor: 'rgb(239 68 68 / 0.25)' }, - '.cm-changedLine': { backgroundColor: 'var(--color-warn-bg)' }, - '.cm-changedText': { backgroundColor: 'rgb(245 158 11 / 0.25)' }, - '.cm-insertedLine': { backgroundColor: 'var(--color-add-bg)' }, - '.cm-deletedLine': { backgroundColor: 'var(--color-del-bg)' }, - '.cm-deletedChunk': { backgroundColor: 'var(--color-del-bg)' }, - '.cm-deletedChunk .cm-deletedText': { backgroundColor: 'rgb(239 68 68 / 0.25)' }, + '.cm-collapsedLines': { + backgroundColor: 'var(--color-surface)', + color: 'var(--color-muted)', + padding: '2px 8px', + fontSize: '11px', + letterSpacing: '0.02em', + cursor: 'pointer', + borderTop: '1px solid var(--color-border)', + borderBottom: '1px solid var(--color-border)', + }, + '.cm-collapsedLines:hover': { color: 'var(--color-fg)' }, - '.cm-collapsedLines': { - backgroundColor: 'var(--color-surface)', - color: 'var(--color-muted)', - padding: '2px 8px', - fontSize: '11px', - letterSpacing: '0.02em', - cursor: 'pointer', - borderTop: '1px solid var(--color-border)', - borderBottom: '1px solid var(--color-border)', + '.cm-merge-gap': { backgroundColor: 'var(--color-gutter)' }, + '.cm-merge-spacer': { backgroundColor: 'var(--color-bg)' }, }, - '.cm-collapsedLines:hover': { color: 'var(--color-fg)' }, - - '.cm-merge-gap': { backgroundColor: 'var(--color-gutter)' }, - '.cm-merge-spacer': { backgroundColor: 'var(--color-bg)' }, - }, - { dark: true }, -) + { dark: true }, + ), +] From e0a7e609b0f04e6bd472d0ce5ba500d58d53bf98 Mon Sep 17 00:00:00 2001 From: fadeltd Date: Fri, 30 Oct 2026 09:20:58 +0700 Subject: [PATCH 2/6] CI: budget first-load JS, not just the entry chunk The old check gzipped only index-*.js, so the registry chunk index.html modulepreloads (~13.8 kB gzip) was never measured. The step now sums every script dist/index.html loads (entry, modulepreloads, registerSW.js): 98,355 B today against a 107,520 B (105 KiB) budget, the same headroom as before. --- .github/pull_request_template.md | 2 +- .github/workflows/ci.yml | 32 +++++++++++++++++++++----------- CLAUDE.md | 2 +- CONTRIBUTING.md | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9ed2250..57d72ec 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,6 +18,6 @@ - [ ] User-facing change → an entry under `## [Unreleased]` in `CHANGELOG.md` (without one, the merge releases nothing) - [ ] No network request, third-party resource, `innerHTML`, or dependency that needs `eval` - [ ] Anything touching a secret declares `store: { kind: 'none' }`; nothing multi-MB goes to `localStorage` -- [ ] Entry bundle still under ~90 kB gzip (CI enforces 92160 B) +- [ ] First-load JS still under 105 KiB gzip (CI enforces 107520 B) - [ ] Works at phone width: panes stack, 44px touch targets, no hover-only controls - [ ] Markdown prose is not hard-wrapped diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eccc450..554a576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,17 +72,27 @@ jobs: exit 1 fi - # The entry bundle is what every visitor pays for on first load. If a - # heavy dependency escapes a lazy route chunk, this is where we find out - # rather than in production. - - name: Check entry bundle budget + # First-load JS is what every visitor pays for before any tool runs: the + # entry plus every chunk the HTML modulepreloads (the registry sits in + # one) and registerSW.js. If a heavy dependency escapes a lazy route + # chunk, this is where we find out rather than in production. + - name: Check first-load JS budget run: | - BUDGET=92160 - ENTRY=$(find dist/assets -name 'index-*.js' -print -quit) - SIZE=$(gzip -c "$ENTRY" | wc -c | tr -d ' ') - echo "entry: $ENTRY" - echo "gzipped: ${SIZE}B budget: ${BUDGET}B" - if [ "$SIZE" -gt "$BUDGET" ]; then - echo "::error::Entry bundle ${SIZE}B exceeds the ${BUDGET}B budget." + BUDGET=107520 + LIST="$RUNNER_TEMP/first-load.txt" + grep -oE '(src|href)="/[^"]+\.js"' dist/index.html | sed -E 's/^(src|href)="\///; s/"$//' > "$LIST" || true + if ! grep -qE '^assets/index-[^/]+\.js$' "$LIST"; then + echo "::error::No entry script found in dist/index.html." + exit 1 + fi + TOTAL=0 + while read -r FILE; do + SIZE=$(gzip -c "dist/$FILE" | wc -c | tr -d ' ') + echo "${SIZE}B $FILE" + TOTAL=$((TOTAL + SIZE)) + done < "$LIST" + echo "first-load JS, gzipped: ${TOTAL}B budget: ${BUDGET}B" + if [ "$TOTAL" -gt "$BUDGET" ]; then + echo "::error::First-load JS ${TOTAL}B exceeds the ${BUDGET}B budget." exit 1 fi diff --git a/CLAUDE.md b/CLAUDE.md index b182b75..1a79c7a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ Load-bearing rules from it: - **A plain `