From 83c104a7441f9806844e1fa45b68ce46d4910a27 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:32:07 +0200 Subject: [PATCH 1/7] chore(app): add Prettier, import sorting, and CI lint/type-check gates - Add prettier + eslint-config-prettier (formatting, run separately from ESLint) - Add eslint-plugin-perfectionist (sorted imports with semantic groups) and eslint-plugin-unused-imports - Replace ~60 hand-maintained browser globals in eslint.config.js with the globals package; disable no-undef for TS files (tsc covers it) - Add tsconfig.test.json so test files are finally type-checked - Add vite-plugin-checker: TS + ESLint overlay during yarn dev (addresses the latent-tsc-error-breaks-Cloud-Build failure mode) - New scripts: lint:fix, fm:check, fm:fix, fix:all; type-check now covers tests - CI: test-frontend job now runs lint, fm:check, and type-check before tests Part 1 of the frontend modernization roadmap (Minimal-template-inspired structure; patterns only, no template code). Co-Authored-By: Claude Fable 5 --- .github/workflows/ci-tests.yml | 15 ++++ app/.prettierignore | 6 ++ app/eslint.config.js | 124 ++++++++++----------------------- app/package.json | 12 +++- app/prettier.config.mjs | 13 ++++ app/tsconfig.test.json | 8 +++ app/vite.config.ts | 9 +++ app/yarn.lock | 107 +++++++++++++++++++++++++++- 8 files changed, 204 insertions(+), 90 deletions(-) create mode 100644 app/.prettierignore create mode 100644 app/prettier.config.mjs create mode 100644 app/tsconfig.test.json diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 135ec93c33..4471e921f9 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -218,6 +218,21 @@ jobs: working-directory: app run: yarn install --frozen-lockfile + - name: Lint frontend + if: steps.check.outputs.should_test == 'true' + working-directory: app + run: yarn lint + + - name: Check frontend formatting + if: steps.check.outputs.should_test == 'true' + working-directory: app + run: yarn fm:check + + - name: Type-check frontend (app + tests) + if: steps.check.outputs.should_test == 'true' + working-directory: app + run: yarn type-check + - name: Run frontend tests with coverage if: steps.check.outputs.should_test == 'true' working-directory: app diff --git a/app/.prettierignore b/app/.prettierignore new file mode 100644 index 0000000000..d17a2d4203 --- /dev/null +++ b/app/.prettierignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +coverage/ +yarn.lock +# Hand-tuned inline critical CSS + meta tags — keep formatting as-is +index.html diff --git a/app/eslint.config.js b/app/eslint.config.js index 866ad99bcb..876fcf162b 100644 --- a/app/eslint.config.js +++ b/app/eslint.config.js @@ -1,8 +1,12 @@ import js from '@eslint/js'; import tseslint from '@typescript-eslint/eslint-plugin'; import tsparser from '@typescript-eslint/parser'; +import prettierConfig from 'eslint-config-prettier'; +import perfectionist from 'eslint-plugin-perfectionist'; import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; +import unusedImports from 'eslint-plugin-unused-imports'; +import globals from 'globals'; export default [ js.configs.recommended, @@ -17,80 +21,14 @@ export default [ jsx: true, }, }, - globals: { - // Browser globals - window: 'readonly', - document: 'readonly', - navigator: 'readonly', - console: 'readonly', - setTimeout: 'readonly', - clearTimeout: 'readonly', - setInterval: 'readonly', - clearInterval: 'readonly', - fetch: 'readonly', - URL: 'readonly', - URLSearchParams: 'readonly', - localStorage: 'readonly', - sessionStorage: 'readonly', - Storage: 'readonly', - history: 'readonly', - location: 'readonly', - requestAnimationFrame: 'readonly', - cancelAnimationFrame: 'readonly', - performance: 'readonly', - crypto: 'readonly', - // DOM types - HTMLElement: 'readonly', - HTMLDivElement: 'readonly', - HTMLInputElement: 'readonly', - HTMLSelectElement: 'readonly', - HTMLButtonElement: 'readonly', - HTMLAnchorElement: 'readonly', - HTMLImageElement: 'readonly', - HTMLCanvasElement: 'readonly', - HTMLIFrameElement: 'readonly', - Element: 'readonly', - Node: 'readonly', - NodeList: 'readonly', - // Events - MouseEvent: 'readonly', - KeyboardEvent: 'readonly', - TouchEvent: 'readonly', - ClipboardEvent: 'readonly', - Event: 'readonly', - MessageEvent: 'readonly', - MediaQueryListEvent: 'readonly', - // APIs - AbortController: 'readonly', - AbortSignal: 'readonly', - RequestInit: 'readonly', - Response: 'readonly', - ResizeObserver: 'readonly', - IntersectionObserver: 'readonly', - MutationObserver: 'readonly', - Blob: 'readonly', - File: 'readonly', - FileReader: 'readonly', - // Idle / animation callback APIs - requestIdleCallback: 'readonly', - cancelIdleCallback: 'readonly', - IdleRequestCallback: 'readonly', - IdleDeadline: 'readonly', - IdleRequestOptions: 'readonly', - IdleCallbackHandle: 'readonly', - FrameRequestCallback: 'readonly', - // IntersectionObserver type aliases - IntersectionObserverCallback: 'readonly', - IntersectionObserverInit: 'readonly', - IntersectionObserverEntry: 'readonly', - // React (for JSX runtime) - React: 'readonly', - }, + globals: globals.browser, }, plugins: { '@typescript-eslint': tseslint, 'react-hooks': reactHooks, 'react-refresh': reactRefresh, + perfectionist, + 'unused-imports': unusedImports, }, rules: { ...tseslint.configs.recommended.rules, @@ -98,29 +36,39 @@ export default [ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 'no-unused-vars': 'off', + // TypeScript reports undefined identifiers itself (incl. DOM lib types); + // with no-undef active every DOM global would need hand-listing here. + 'no-undef': 'off', // Allow ref updates during render (common pattern for keeping refs in sync) 'react-hooks/refs': 'off', + 'unused-imports/no-unused-imports': 'error', + 'perfectionist/sort-imports': [ + 'error', + { + type: 'natural', + newlinesBetween: 1, + internalPattern: ['^src/'], + groups: [ + 'side-effect-style', + 'side-effect', + 'react', + ['builtin', 'external'], + 'mui', + 'internal', + ['parent', 'sibling', 'index'], + 'unknown', + ], + customGroups: [ + { groupName: 'react', elementNamePattern: ['^react$', '^react-dom'] }, + { groupName: 'mui', elementNamePattern: ['^@mui/'] }, + ], + }, + ], + 'perfectionist/sort-named-imports': ['error', { type: 'natural' }], }, }, { - files: ['src/**/*.test.{ts,tsx}'], - languageOptions: { - globals: { - // Vitest globals - describe: 'readonly', - it: 'readonly', - expect: 'readonly', - vi: 'readonly', - beforeEach: 'readonly', - afterEach: 'readonly', - beforeAll: 'readonly', - afterAll: 'readonly', - globalThis: 'readonly', - global: 'readonly', - }, - }, - }, - { - ignores: ['dist/**', 'node_modules/**', '*.config.js'], + ignores: ['dist/**', 'node_modules/**', 'coverage/**', '*.config.js', '*.config.mjs'], }, + prettierConfig, ]; diff --git a/app/package.json b/app/package.json index 5cb8d87e07..0b5c09025e 100644 --- a/app/package.json +++ b/app/package.json @@ -9,7 +9,11 @@ "build": "tsc && vite build", "preview": "vite preview", "lint": "eslint src", - "type-check": "tsc --noEmit", + "lint:fix": "eslint src --fix", + "fm:check": "prettier --check .", + "fm:fix": "prettier --write .", + "fix:all": "yarn lint:fix && yarn fm:fix", + "type-check": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit", "test": "vitest run", "test:watch": "vitest" }, @@ -46,11 +50,17 @@ "@vitejs/plugin-react-swc": "^4.3.1", "@vitest/coverage-v8": "^4.1.8", "eslint": "^10.4.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", + "eslint-plugin-unused-imports": "^4.4.1", + "globals": "^17.6.0", "jsdom": "^29.1.1", + "prettier": "^3.8.4", "typescript": "^6.0.3", "vite": "^8.0.16", + "vite-plugin-checker": "^0.14.1", "vite-plugin-compression2": "^2.5.3", "vitest": "^4.1.8" } diff --git a/app/prettier.config.mjs b/app/prettier.config.mjs new file mode 100644 index 0000000000..057cbca8c3 --- /dev/null +++ b/app/prettier.config.mjs @@ -0,0 +1,13 @@ +/** + * Prettier configuration for the anyplot frontend. + * Values match the predominant existing style to minimize reformat churn. + */ +export default { + printWidth: 100, + tabWidth: 2, + singleQuote: true, + semi: true, + trailingComma: 'es5', + arrowParens: 'avoid', + endOfLine: 'lf', +}; diff --git a/app/tsconfig.test.json b/app/tsconfig.test.json new file mode 100644 index 0000000000..d745adc0de --- /dev/null +++ b/app/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src"], + "exclude": [] +} diff --git a/app/vite.config.ts b/app/vite.config.ts index 1a61498312..75e85c7dae 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -1,10 +1,19 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; +import { checker } from 'vite-plugin-checker'; import { compression } from 'vite-plugin-compression2'; export default defineConfig({ plugins: [ react(), + // Dev-only TS + ESLint feedback in the browser overlay; the production + // build already type-checks via `tsc && vite build`. + checker({ + typescript: true, + eslint: { lintCommand: 'eslint src', useFlatConfig: true }, + overlay: { initialIsOpen: false }, + enableBuild: false, + }), compression({ algorithm: 'gzip', threshold: 1024 }), compression({ algorithm: 'brotliCompress', threshold: 1024 }), ], diff --git a/app/yarn.lock b/app/yarn.lock index d43ee3b76e..a8c487d0b2 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -981,7 +981,7 @@ tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.61.0": +"@typescript-eslint/utils@8.61.0", "@typescript-eslint/utils@^8.58.2": version "8.61.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f" integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA== @@ -1230,6 +1230,13 @@ character-reference-invalid@^2.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== + dependencies: + readdirp "^5.0.0" + clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" @@ -1518,6 +1525,19 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-config-prettier@^10.1.8: + version "10.1.8" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97" + integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w== + +eslint-plugin-perfectionist@^5.9.0: + version "5.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.9.0.tgz#d8844892fa4b1069df27910f41d7e7981df1ca4b" + integrity sha512-8TWzg02zmnBdZwCkWLi8jhzqXI+fE7Z/RwV8SL6xD45tJ8Bp3wGuYL2XtQgfe/Wd0eBqOUX+s6ey73IyszvKTA== + dependencies: + "@typescript-eslint/utils" "^8.58.2" + natural-orderby "^5.0.0" + eslint-plugin-react-hooks@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz#e6742cad75d970c0a3f30d7d3fa80a4784f55927" @@ -1534,6 +1554,11 @@ eslint-plugin-react-refresh@^0.5.2: resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz#39e11021be10e1cd9adab2bdeabc65b17222409f" integrity sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA== +eslint-plugin-unused-imports@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz#a831f0a2937d7631eba30cb87091ab7d3a5da0e1" + integrity sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ== + eslint-scope@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.2.tgz#b9de6ace2fab1cff24d2e58d85b74c8fcea39802" @@ -1762,6 +1787,16 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +globals@^17.6.0: + version "17.6.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-17.6.0.tgz#0f0be018d5cca8690e6375ead1f65c4bb96191fc" + integrity sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA== + +graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -2229,11 +2264,24 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +natural-orderby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-5.0.0.tgz#bb655f669ee9c84e82cdc6cddbba25eb263cd9f4" + integrity sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg== + node-releases@^2.0.36: version "2.0.37" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.37.tgz#9bd4f10b77ba39c2b9402d4e8399c482a797f671" integrity sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg== +npm-run-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-6.0.0.tgz#25cfdc4eae04976f3349c0b1afc089052c362537" + integrity sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA== + dependencies: + path-key "^4.0.0" + unicorn-magic "^0.3.0" + object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2317,6 +2365,11 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -2361,6 +2414,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^3.8.4: + version "3.8.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.4.tgz#f334f013ac04a96676f24dabc23c1c4ae1bae411" + integrity sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q== + pretty-format@^27.0.2: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" @@ -2384,6 +2442,15 @@ prop-types@15, prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +proper-lockfile@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + property-information@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" @@ -2488,6 +2555,11 @@ react@^19.2.6: resolved "https://registry.yarnpkg.com/react/-/react-19.2.6.tgz#3dadb8e12b2a7934c1d5317973e5dce1301f9a4d" integrity sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q== +readdirp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.0.0.tgz#fbf1f71a727891d685bb1786f9ba74084f6e2f91" + integrity sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ== + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -2525,6 +2597,11 @@ resolve@^1.19.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + rolldown@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac" @@ -2598,6 +2675,11 @@ siginfo@^2.0.0: resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" @@ -2657,6 +2739,11 @@ tar-mini@^0.2.0: resolved "https://registry.yarnpkg.com/tar-mini/-/tar-mini-0.2.0.tgz#2b2cdc215f5b83b0ab8ce363dc9ded22de51849b" integrity sha512-+qfUHz700DWnRutdUsxRRVZ38G1Qr27OetwaMYTdg8hcPxf46U0S1Zf76dQMWRBmusOt2ZCK5kbIaiLkoGO7WQ== +tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tinybench@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" @@ -2738,6 +2825,11 @@ undici@^7.25.0: resolved "https://registry.yarnpkg.com/undici/-/undici-7.25.0.tgz#7d72fc429a0421769ca2966fd07cac875c85b781" integrity sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ== +unicorn-magic@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz#4efd45c85a69e0dd576d25532fbfa22aa5c8a104" + integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA== + update-browserslist-db@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" @@ -2753,6 +2845,19 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +vite-plugin-checker@^0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.14.1.tgz#0fe694c6d90905c9da7658772fadab4823d7b881" + integrity sha512-Mv8oQc9XYBYf+XkP/riqqQCt8lBP6Iad75PZPho1lHRrpxQI0BwX2gwE10enn4f6Hgc+PvR1F7N38KARcaJtzw== + dependencies: + "@babel/code-frame" "^7.29.0" + chokidar "^5.0.0" + npm-run-path "^6.0.0" + picocolors "^1.1.1" + picomatch "^4.0.4" + proper-lockfile "^4.1.2" + tiny-invariant "^1.3.3" + vite-plugin-compression2@^2.5.3: version "2.5.3" resolved "https://registry.yarnpkg.com/vite-plugin-compression2/-/vite-plugin-compression2-2.5.3.tgz#cd8b4f8e0fcbc21d937b3b8f39895bdd475c5dd9" From 2bfc40b03d344b9dbdba7b4d3cc2a1f84088e7d7 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:35:22 +0200 Subject: [PATCH 2/7] style(app): one-time mechanical reformat (eslint --fix + prettier) Pure formatting commit, no logic changes: sorted imports (perfectionist groups) and Prettier formatting across app/ sources. Co-Authored-By: Claude Fable 5 --- app/cloudbuild.yaml | 98 +- app/src/analytics/reportWebVitals.test.ts | 17 +- app/src/analytics/reportWebVitals.ts | 92 +- app/src/components/BareLayout.tsx | 1 + app/src/components/CodeHighlighter.test.tsx | 37 +- app/src/components/CodeHighlighter.tsx | 13 +- app/src/components/CodeShowcase.tsx | 193 ++- app/src/components/ErrorBoundary.test.tsx | 4 +- app/src/components/ErrorBoundary.tsx | 13 +- app/src/components/FeedbackWidget.test.tsx | 18 +- app/src/components/FeedbackWidget.tsx | 69 +- app/src/components/FilterBar.test.tsx | 28 +- app/src/components/FilterBar.tsx | 452 +++--- app/src/components/Footer.test.tsx | 41 +- app/src/components/Footer.tsx | 49 +- app/src/components/HeroSection.test.tsx | 13 +- app/src/components/HeroSection.tsx | 43 +- app/src/components/ImageCard.test.tsx | 10 +- app/src/components/ImageCard.tsx | 164 +- app/src/components/ImagesGrid.test.tsx | 33 +- app/src/components/ImagesGrid.tsx | 50 +- app/src/components/Layout.test.tsx | 33 +- app/src/components/Layout.tsx | 21 +- app/src/components/LibrariesSection.tsx | 23 +- app/src/components/LibraryCard.test.tsx | 3 +- app/src/components/LibraryCard.tsx | 110 +- app/src/components/LibraryPills.test.tsx | 3 +- app/src/components/LibraryPills.tsx | 182 ++- app/src/components/LoaderSpinner.test.tsx | 3 +- app/src/components/LoaderSpinner.tsx | 1 + app/src/components/MastheadRule.test.tsx | 25 +- app/src/components/MastheadRule.tsx | 136 +- app/src/components/NavBar.test.tsx | 3 +- app/src/components/NavBar.tsx | 90 +- app/src/components/NumbersStrip.tsx | 61 +- app/src/components/PaletteStrip.tsx | 43 +- app/src/components/PlotOfTheDay.test.tsx | 36 +- app/src/components/PlotOfTheDay.tsx | 338 +++-- .../components/PlotOfTheDayTerminal.test.tsx | 24 +- app/src/components/PlotOfTheDayTerminal.tsx | 83 +- app/src/components/RelatedSpecs.test.tsx | 7 +- app/src/components/RelatedSpecs.tsx | 233 ++- app/src/components/RootLayout.test.tsx | 16 +- app/src/components/RootLayout.tsx | 20 +- .../components/RouteErrorBoundary.test.tsx | 18 +- app/src/components/RouteErrorBoundary.tsx | 9 +- app/src/components/ScienceNote.tsx | 133 +- app/src/components/SectionHeader.test.tsx | 10 +- app/src/components/SectionHeader.tsx | 82 +- app/src/components/SpecDetailView.test.tsx | 54 +- app/src/components/SpecDetailView.tsx | 152 +- app/src/components/SpecOverview.test.tsx | 26 +- app/src/components/SpecOverview.tsx | 85 +- app/src/components/SpecTabs.test.tsx | 62 +- app/src/components/SpecTabs.tsx | 611 +++++--- app/src/components/ThemeToggle.tsx | 3 +- app/src/components/ToolbarActions.test.tsx | 27 +- app/src/components/ToolbarActions.tsx | 19 +- app/src/components/TypewriterText.tsx | 2 + app/src/constants/index.ts | 18 +- app/src/data/paletteMatrices.json | 661 +------- app/src/hooks/index.ts | 7 +- app/src/hooks/useAnalytics.test.ts | 14 +- app/src/hooks/useAnalytics.ts | 69 +- app/src/hooks/useCodeFetch.test.ts | 12 +- app/src/hooks/useCodeFetch.ts | 94 +- app/src/hooks/useCopyCode.test.ts | 9 +- app/src/hooks/useCopyCode.ts | 2 +- app/src/hooks/useFeaturedSpecs.ts | 9 +- app/src/hooks/useFilterFetch.test.ts | 57 +- app/src/hooks/useFilterFetch.ts | 20 +- app/src/hooks/useFilterState-extended.test.ts | 9 +- .../hooks/useFilterState-imagesKey.test.ts | 5 +- app/src/hooks/useFilterState.test.ts | 13 +- app/src/hooks/useFilterState.ts | 31 +- app/src/hooks/useInfiniteScroll.test.ts | 72 +- app/src/hooks/useInfiniteScroll.ts | 9 +- app/src/hooks/useLatestRelease.test.ts | 28 +- app/src/hooks/useLatestRelease.ts | 9 +- app/src/hooks/useLayoutContext.test.ts | 5 +- app/src/hooks/useLayoutContext.ts | 3 +- app/src/hooks/useLocalStorage.test.ts | 7 +- app/src/hooks/useLocalStorage.ts | 13 +- app/src/hooks/usePlotOfTheDay.ts | 1 + app/src/hooks/useThemeMode.test.ts | 49 +- app/src/hooks/useThemeMode.ts | 14 +- app/src/hooks/useTypewriter.ts | 4 +- app/src/hooks/useUrlSync.test.ts | 5 +- app/src/hooks/useUrlSync.ts | 10 +- app/src/main.tsx | 18 +- app/src/pages/AboutPage.tsx | 96 +- app/src/pages/DebugPage.test.tsx | 191 ++- app/src/pages/DebugPage.tsx | 1084 ++++++++++---- app/src/pages/LandingPage.test.tsx | 35 +- app/src/pages/LandingPage.tsx | 164 +- app/src/pages/LegalPage.test.tsx | 6 +- app/src/pages/LegalPage.tsx | 206 ++- app/src/pages/LibrariesPage.test.tsx | 9 +- app/src/pages/LibrariesPage.tsx | 71 +- app/src/pages/MapPage.helpers.test.ts | 83 +- app/src/pages/MapPage.helpers.ts | 19 +- app/src/pages/MapPage.test.tsx | 98 +- app/src/pages/MapPage.tsx | 472 +++--- app/src/pages/McpPage.test.tsx | 19 +- app/src/pages/McpPage.tsx | 141 +- app/src/pages/NotFoundPage.test.tsx | 3 +- app/src/pages/NotFoundPage.tsx | 22 +- app/src/pages/PalettePage.snippet.test.ts | 10 +- app/src/pages/PalettePage.tsx | 1331 +++++++++++++---- app/src/pages/PlotsPage.test.tsx | 7 +- app/src/pages/PlotsPage.tsx | 27 +- app/src/pages/SpecPage.test.tsx | 7 +- app/src/pages/SpecPage.tsx | 293 ++-- app/src/pages/SpecsListPage.test.tsx | 21 +- app/src/pages/SpecsListPage.tsx | 78 +- app/src/pages/StatsPage.test.tsx | 11 +- app/src/pages/StatsPage.tsx | 730 ++++++--- app/src/router.tsx | 71 +- app/src/styles/fonts.css | 192 ++- app/src/styles/tokens.css | 142 +- app/src/test-utils.tsx | 6 +- app/src/theme/index.ts | 23 +- app/src/types/d3-force-3d.d.ts | 8 +- app/src/types/index.ts | 8 +- app/src/utils/claudePrompt.test.ts | 2 +- app/src/utils/claudePrompt.ts | 21 +- app/src/utils/filters-extended.test.ts | 47 +- app/src/utils/filters.test.ts | 25 +- app/src/utils/filters.ts | 10 +- app/src/utils/fuzzySearch.test.ts | 3 +- app/src/utils/fuzzySearch.ts | 2 +- app/src/utils/responsiveImage.test.ts | 17 +- app/src/utils/responsiveImage.ts | 6 +- app/src/utils/shuffle.test.ts | 3 +- app/src/utils/themedPreview.ts | 28 +- app/src/utils/tooltip.test.ts | 17 +- app/src/utils/tooltip.ts | 6 +- app/vite.config.ts | 7 +- 138 files changed, 7135 insertions(+), 4245 deletions(-) diff --git a/app/cloudbuild.yaml b/app/cloudbuild.yaml index 684d364759..d749fb7219 100644 --- a/app/cloudbuild.yaml +++ b/app/cloudbuild.yaml @@ -1,74 +1,74 @@ # Cloud Build configuration for anyplot Frontend substitutions: - _REGION: "europe-west4" - _SERVICE_NAME: "anyplot-app" - _VITE_API_URL: "https://api.anyplot.ai" + _REGION: 'europe-west4' + _SERVICE_NAME: 'anyplot-app' + _VITE_API_URL: 'https://api.anyplot.ai' # Only DebugPage uses this — same-origin via the Cloudflare Worker on # anyplot.ai/api/*, so the CF Access cookie on anyplot.ai is sent with # fetch (host-only cookies cannot cross subdomains). Other pages keep # using VITE_API_URL directly because their endpoints are public and # don't need the CF Access cookie. - _VITE_DEBUG_API_URL: "/api" + _VITE_DEBUG_API_URL: '/api' steps: # Build the container image - - name: "gcr.io/cloud-builders/docker" + - name: 'gcr.io/cloud-builders/docker' args: [ - "build", - "-t", - "europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:$BUILD_ID", - "-t", - "europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:latest", - "--build-arg", - "VITE_API_URL=${_VITE_API_URL}", - "--build-arg", - "VITE_DEBUG_API_URL=${_VITE_DEBUG_API_URL}", - "-f", - "app/Dockerfile", - "app", + 'build', + '-t', + 'europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:$BUILD_ID', + '-t', + 'europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:latest', + '--build-arg', + 'VITE_API_URL=${_VITE_API_URL}', + '--build-arg', + 'VITE_DEBUG_API_URL=${_VITE_DEBUG_API_URL}', + '-f', + 'app/Dockerfile', + 'app', ] # Push the container image to Artifact Registry - - name: "gcr.io/cloud-builders/docker" - args: ["push", "--all-tags", "europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}"] + - name: 'gcr.io/cloud-builders/docker' + args: ['push', '--all-tags', 'europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}'] # Deploy container image to Cloud Run - - name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' entrypoint: gcloud args: - - "run" - - "deploy" - - "${_SERVICE_NAME}" - - "--image" - - "europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:$BUILD_ID" - - "--region" - - "${_REGION}" - - "--platform" - - "managed" - - "--allow-unauthenticated" - - "--memory" - - "512Mi" - - "--cpu" - - "1" - - "--timeout" - - "60" - - "--min-instances" - - "1" - - "--max-instances" - - "3" - - "--port" - - "8080" - - "--execution-environment" - - "gen2" - - "--cpu-throttling" - - "--concurrency" - - "15" + - 'run' + - 'deploy' + - '${_SERVICE_NAME}' + - '--image' + - 'europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:$BUILD_ID' + - '--region' + - '${_REGION}' + - '--platform' + - 'managed' + - '--allow-unauthenticated' + - '--memory' + - '512Mi' + - '--cpu' + - '1' + - '--timeout' + - '60' + - '--min-instances' + - '1' + - '--max-instances' + - '3' + - '--port' + - '8080' + - '--execution-environment' + - 'gen2' + - '--cpu-throttling' + - '--concurrency' + - '15' # Store images in Artifact Registry images: - - "europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:$BUILD_ID" - - "europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:latest" + - 'europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:$BUILD_ID' + - 'europe-west4-docker.pkg.dev/$PROJECT_ID/anyplot/${_SERVICE_NAME}:latest' options: logging: CLOUD_LOGGING_ONLY diff --git a/app/src/analytics/reportWebVitals.test.ts b/app/src/analytics/reportWebVitals.test.ts index abd49c0e21..5c17818d68 100644 --- a/app/src/analytics/reportWebVitals.test.ts +++ b/app/src/analytics/reportWebVitals.test.ts @@ -1,15 +1,20 @@ -import { describe, it, expect, vi, afterEach } from 'vitest'; -import { reportWebVitals } from './reportWebVitals'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + import { setAnalyticsAmbientProps } from '../hooks/useAnalytics'; +import { reportWebVitals } from './reportWebVitals'; // Single hoisted mock (vi.mock dedupes by module path — last call wins, so // keeping one shared mock avoids cross-test interference). vi.mock('web-vitals', () => ({ - onLCP: (cb: (m: { value: number; rating: string }) => void) => cb({ value: 2500, rating: 'good' }), - onCLS: (cb: (m: { value: number; rating: string }) => void) => cb({ value: 0.15, rating: 'needs-improvement' }), + onLCP: (cb: (m: { value: number; rating: string }) => void) => + cb({ value: 2500, rating: 'good' }), + onCLS: (cb: (m: { value: number; rating: string }) => void) => + cb({ value: 0.15, rating: 'needs-improvement' }), onINP: (cb: (m: { value: number; rating: string }) => void) => cb({ value: 200, rating: 'good' }), - onFCP: (cb: (m: { value: number; rating: string }) => void) => cb({ value: 1200, rating: 'good' }), - onTTFB: (cb: (m: { value: number; rating: string }) => void) => cb({ value: 400, rating: 'good' }), + onFCP: (cb: (m: { value: number; rating: string }) => void) => + cb({ value: 1200, rating: 'good' }), + onTTFB: (cb: (m: { value: number; rating: string }) => void) => + cb({ value: 400, rating: 'good' }), })); describe('reportWebVitals', () => { diff --git a/app/src/analytics/reportWebVitals.ts b/app/src/analytics/reportWebVitals.ts index ce7e3402e5..809b0dc4af 100644 --- a/app/src/analytics/reportWebVitals.ts +++ b/app/src/analytics/reportWebVitals.ts @@ -6,63 +6,61 @@ import { getAnalyticsAmbientProps } from '../hooks/useAnalytics'; * Only runs in production (anyplot.ai), dynamically imported for zero dev cost. */ export function reportWebVitals() { - if ( - typeof window === 'undefined' || - window.location.hostname !== 'anyplot.ai' - ) { + if (typeof window === 'undefined' || window.location.hostname !== 'anyplot.ai') { return; } - import('web-vitals').then(({ onLCP, onCLS, onINP, onFCP, onTTFB }) => { - onLCP((metric) => { - window.plausible?.('LCP', { - props: { - ...getAnalyticsAmbientProps(), - value: String(Math.round(metric.value / 100) * 100), - rating: metric.rating, - }, + import('web-vitals') + .then(({ onLCP, onCLS, onINP, onFCP, onTTFB }) => { + onLCP(metric => { + window.plausible?.('LCP', { + props: { + ...getAnalyticsAmbientProps(), + value: String(Math.round(metric.value / 100) * 100), + rating: metric.rating, + }, + }); }); - }); - onCLS((metric) => { - window.plausible?.('CLS', { - props: { - ...getAnalyticsAmbientProps(), - value: String(Math.round(metric.value * 100) / 100), - rating: metric.rating, - }, + onCLS(metric => { + window.plausible?.('CLS', { + props: { + ...getAnalyticsAmbientProps(), + value: String(Math.round(metric.value * 100) / 100), + rating: metric.rating, + }, + }); }); - }); - onINP((metric) => { - window.plausible?.('INP', { - props: { - ...getAnalyticsAmbientProps(), - value: String(Math.round(metric.value / 50) * 50), - rating: metric.rating, - }, + onINP(metric => { + window.plausible?.('INP', { + props: { + ...getAnalyticsAmbientProps(), + value: String(Math.round(metric.value / 50) * 50), + rating: metric.rating, + }, + }); }); - }); - onFCP((metric) => { - window.plausible?.('FCP', { - props: { - ...getAnalyticsAmbientProps(), - value: String(Math.round(metric.value / 100) * 100), - rating: metric.rating, - }, + onFCP(metric => { + window.plausible?.('FCP', { + props: { + ...getAnalyticsAmbientProps(), + value: String(Math.round(metric.value / 100) * 100), + rating: metric.rating, + }, + }); }); - }); - onTTFB((metric) => { - window.plausible?.('TTFB', { - props: { - ...getAnalyticsAmbientProps(), - value: String(Math.round(metric.value / 100) * 100), - rating: metric.rating, - }, + onTTFB(metric => { + window.plausible?.('TTFB', { + props: { + ...getAnalyticsAmbientProps(), + value: String(Math.round(metric.value / 100) * 100), + rating: metric.rating, + }, + }); }); - }); - }) - .catch(() => {}); + }) + .catch(() => {}); } diff --git a/app/src/components/BareLayout.tsx b/app/src/components/BareLayout.tsx index dc8f08b3a1..b1845f2f3e 100644 --- a/app/src/components/BareLayout.tsx +++ b/app/src/components/BareLayout.tsx @@ -1,4 +1,5 @@ import { Outlet } from 'react-router-dom'; + import Box from '@mui/material/Box'; /** diff --git a/app/src/components/CodeHighlighter.test.tsx b/app/src/components/CodeHighlighter.test.tsx index 810261ad74..e59deee2aa 100644 --- a/app/src/components/CodeHighlighter.test.tsx +++ b/app/src/components/CodeHighlighter.test.tsx @@ -1,4 +1,5 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; + import { render, screen } from '../test-utils'; vi.mock('react-syntax-highlighter/dist/esm/prism-light', () => { @@ -62,34 +63,22 @@ describe('CodeHighlighter', () => { it('defaults to python when no language prop given', () => { render(); - expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( - 'data-language', - 'python' - ); + expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute('data-language', 'python'); }); it('uses r grammar when language is "r"', () => { - render(); - expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( - 'data-language', - 'r' - ); + render(); + expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute('data-language', 'r'); }); it('uses julia grammar when language is "julia"', () => { - render(); - expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( - 'data-language', - 'julia' - ); + render(); + expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute('data-language', 'julia'); }); it('falls back to plain text for unknown languages', () => { render(); - expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( - 'data-language', - 'text' - ); + expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute('data-language', 'text'); }); it('uses the tsx grammar for the muix library (React TSX) even though its language is javascript', () => { @@ -100,17 +89,11 @@ describe('CodeHighlighter', () => { library="muix" /> ); - expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( - 'data-language', - 'tsx' - ); + expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute('data-language', 'tsx'); }); it('uses the language grammar when the library has no grammar override', () => { render(); - expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( - 'data-language', - 'javascript' - ); + expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute('data-language', 'javascript'); }); }); diff --git a/app/src/components/CodeHighlighter.tsx b/app/src/components/CodeHighlighter.tsx index be6fa911bc..1b6ccd08f0 100644 --- a/app/src/components/CodeHighlighter.tsx +++ b/app/src/components/CodeHighlighter.tsx @@ -1,9 +1,10 @@ -import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light'; +import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript'; +import julia from 'react-syntax-highlighter/dist/esm/languages/prism/julia'; import python from 'react-syntax-highlighter/dist/esm/languages/prism/python'; import r from 'react-syntax-highlighter/dist/esm/languages/prism/r'; -import julia from 'react-syntax-highlighter/dist/esm/languages/prism/julia'; -import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript'; import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx'; +import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light'; + import { typography } from '../theme'; SyntaxHighlighter.registerLanguage('python', python); @@ -78,7 +79,11 @@ interface CodeHighlighterProps { library?: string; } -export default function CodeHighlighter({ code, language = 'python', library }: CodeHighlighterProps) { +export default function CodeHighlighter({ + code, + language = 'python', + library, +}: CodeHighlighterProps) { const prismLanguage = (library ? LIBRARY_GRAMMAR_OVERRIDE[library.toLowerCase()] : undefined) ?? PRISM_LANGUAGE[language.toLowerCase()] ?? diff --git a/app/src/components/CodeShowcase.tsx b/app/src/components/CodeShowcase.tsx index d839a42777..c9c35fd42e 100644 --- a/app/src/components/CodeShowcase.tsx +++ b/app/src/components/CodeShowcase.tsx @@ -1,118 +1,167 @@ import Box from '@mui/material/Box'; -import { SectionHeader } from './SectionHeader'; + import { typography } from '../theme'; +import { SectionHeader } from './SectionHeader'; export function CodeShowcase() { return ( One import.} + title={ + <> + One import. + + } /> - + {/* Left: description */} - - Same palette,
every library. + + Same palette, +
+ every library.
- - every example in the catalogue uses the same imprint palette. switch libraries - without losing your color grammar — a gentoo penguin is always blue, - whether you draw it in matplotlib or plotly. + + every example in the catalogue uses the same imprint palette. switch libraries without + losing your color grammar — a gentoo penguin is + always blue, whether you draw it in matplotlib or plotly. - - validated against deuteranopia, protanopia and tritanopia using the Machado et al. (2009) simulation model. + + validated against deuteranopia, protanopia and tritanopia using the Machado et al. + (2009) simulation model. {/* Right: code block — terminal showcase, intentionally dark in both themes so the macOS-style window dots and drop shadow stay coherent. */} - + {'# pick any library. the palette travels with you.\n'} - import{' anyplot '} - as{' ap\n\n'} + + import + + {' anyplot '} + + as + + {' ap\n\n'} {'data = ap.'} - load + + load + {'('} - "penguins" + + "penguins" + {')\n\n'} {'# matplotlib\n'} {'ap.'} - mpl + + mpl + {'.'} - scatter + + scatter + {'(data, x='} - "bill" + + "bill" + {', y='} - "flipper" + + "flipper" + {',\n hue='} - "species" + + "species" + {')\n\n'} {'# plotly — same colors, interactive\n'} {'ap.'} - plotly + + plotly + {'.'} - scatter + + scatter + {'(data, x='} - "bill" + + "bill" + {', y='} - "flipper" + + "flipper" + {',\n hue='} - "species" + + "species" + {')'} diff --git a/app/src/components/ErrorBoundary.test.tsx b/app/src/components/ErrorBoundary.test.tsx index 59e27a5061..6c46c6868f 100644 --- a/app/src/components/ErrorBoundary.test.tsx +++ b/app/src/components/ErrorBoundary.test.tsx @@ -1,6 +1,6 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen } from '../test-utils'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '../test-utils'; // Must import after test-utils to get jest-dom matchers import { ErrorBoundary } from './ErrorBoundary'; diff --git a/app/src/components/ErrorBoundary.tsx b/app/src/components/ErrorBoundary.tsx index 2710ddcc96..b220fb158d 100644 --- a/app/src/components/ErrorBoundary.tsx +++ b/app/src/components/ErrorBoundary.tsx @@ -1,12 +1,13 @@ import { Component, ReactNode } from 'react'; -import Box from '@mui/material/Box'; + +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import HomeIcon from '@mui/icons-material/Home'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import ReplayIcon from '@mui/icons-material/Replay'; import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import ReplayIcon from '@mui/icons-material/Replay'; -import HomeIcon from '@mui/icons-material/Home'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; interface Props { children: ReactNode; @@ -67,7 +68,7 @@ export class ErrorBoundary extends Component { }; handleToggleDetails = (): void => { - this.setState((s) => ({ showDetails: !s.showDetails })); + this.setState(s => ({ showDetails: !s.showDetails })); }; buildDetails = (): string => { diff --git a/app/src/components/FeedbackWidget.test.tsx b/app/src/components/FeedbackWidget.test.tsx index 7004790dec..a0c33cc0a5 100644 --- a/app/src/components/FeedbackWidget.test.tsx +++ b/app/src/components/FeedbackWidget.test.tsx @@ -1,4 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { render, screen, userEvent, waitFor } from '../test-utils'; import { FeedbackWidget } from './FeedbackWidget'; @@ -32,7 +33,10 @@ describe('FeedbackWidget', () => { it('submits a reaction-only entry when 👍 is clicked in the mini-stack', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( - new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } }) + new Response(JSON.stringify({ status: 'ok' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) ); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime.bind(vi) }); @@ -78,7 +82,10 @@ describe('FeedbackWidget', () => { it('POSTs full-form fields to /feedback and shows a thank-you on success', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( - new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } }) + new Response(JSON.stringify({ status: 'ok' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) ); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime.bind(vi) }); @@ -157,7 +164,10 @@ describe('FeedbackWidget', () => { it('full-dialog submit sends the chosen reaction in the payload', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( - new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } }) + new Response(JSON.stringify({ status: 'ok' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) ); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime.bind(vi) }); diff --git a/app/src/components/FeedbackWidget.tsx b/app/src/components/FeedbackWidget.tsx index c7a16b8ece..61d66f812b 100644 --- a/app/src/components/FeedbackWidget.tsx +++ b/app/src/components/FeedbackWidget.tsx @@ -1,4 +1,10 @@ import { useEffect, useRef, useState } from 'react'; + +import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutlineOutlined'; +import CloseIcon from '@mui/icons-material/Close'; +import ForumIcon from '@mui/icons-material/ForumOutlined'; +import ThumbDownIcon from '@mui/icons-material/ThumbDownOutlined'; +import ThumbUpIcon from '@mui/icons-material/ThumbUpOutlined'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import ClickAwayListener from '@mui/material/ClickAwayListener'; @@ -9,11 +15,7 @@ import TextField from '@mui/material/TextField'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Tooltip from '@mui/material/Tooltip'; -import ForumIcon from '@mui/icons-material/ForumOutlined'; -import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutlineOutlined'; -import ThumbUpIcon from '@mui/icons-material/ThumbUpOutlined'; -import ThumbDownIcon from '@mui/icons-material/ThumbDownOutlined'; -import CloseIcon from '@mui/icons-material/Close'; + import { API_URL } from '../constants'; import { useAnalytics } from '../hooks'; import { useLocalStorage } from '../hooks/useLocalStorage'; @@ -58,7 +60,7 @@ function newSessionId(): string { if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); - return `s-${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`; + return `s-${Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')}`; } // Browser without Web Crypto support (e.g. very old, or insecure context). The // session id is an opaque correlation handle, not a credential — a coarse @@ -135,7 +137,9 @@ export function FeedbackWidget() { // so the footer's top edge runs exactly through the FAB centre. const FAB_CENTER_FROM_BOTTOM_XS = 32; const liftTransform = - lift > FAB_CENTER_FROM_BOTTOM_XS ? `translateY(-${lift - FAB_CENTER_FROM_BOTTOM_XS}px)` : 'none'; + lift > FAB_CENTER_FROM_BOTTOM_XS + ? `translateY(-${lift - FAB_CENTER_FROM_BOTTOM_XS}px)` + : 'none'; const [sessionId, setSessionId] = useLocalStorage(SESSION_KEY, ''); @@ -198,7 +202,11 @@ export function FeedbackWidget() { if (mode === 'quick') setMode('closed'); }; - const buildPayload = (overrides: { message: string | null; reaction: Reaction | null; contact: string | null }) => ({ + const buildPayload = (overrides: { + message: string | null; + reaction: Reaction | null; + contact: string | null; + }) => ({ message: overrides.message, reaction: overrides.reaction, contact: overrides.contact, @@ -426,15 +434,20 @@ export function FeedbackWidget() { 🙏 Thanks! - - We read every note. - + We read every note. ) : ( - + Quick feedback - + @@ -447,8 +460,10 @@ export function FeedbackWidget() { fullWidth placeholder="Bug, idea, typo, anything…" value={message} - onChange={(e) => setMessage(e.target.value)} - slotProps={{ htmlInput: { maxLength: MAX_MESSAGE_LENGTH, 'aria-label': 'Feedback message' } }} + onChange={e => setMessage(e.target.value)} + slotProps={{ + htmlInput: { maxLength: MAX_MESSAGE_LENGTH, 'aria-label': 'Feedback message' }, + }} disabled={submitting} sx={{ mb: 1.5 }} /> @@ -461,8 +476,13 @@ export function FeedbackWidget() { aria-label="Reaction" sx={{ display: 'flex', flexWrap: 'wrap', mb: 1.5 }} > - {REACTIONS.map((r) => ( - + {REACTIONS.map(r => ( + {r.glyph} ))} @@ -473,7 +493,7 @@ export function FeedbackWidget() { size="small" placeholder="Name or email (optional)" value={contact} - onChange={(e) => setContact(e.target.value)} + onChange={e => setContact(e.target.value)} slotProps={{ htmlInput: { maxLength: 255, 'aria-label': 'Contact (optional)' } }} disabled={submitting} sx={{ mb: 1 }} @@ -494,14 +514,23 @@ export function FeedbackWidget() { {/* Honeypot — real users never see this, bots will fill it and trip the server-side guard. */} -