Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/pages-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ on:
- "cli/scripts/install.sh"
- "cli/scripts/install.ps1"
- ".github/actions/**"
# ``lighthouse.yml``'s ``site`` filter watches the LHCI toolchain
# (``web/package.json`` / ``web/package-lock.json``) so a
# toolchain-only bump revalidates the marketing-site Lighthouse
# score. Mirror those paths here so the same bump also redeploys
# the preview the audit runs against; otherwise ``lighthouse-site``
# times out polling for a banner SHA that was never deployed.
- "web/package.json"
- "web/package-lock.json"
- ".github/workflows/pages-preview.yml"
workflow_dispatch:
inputs:
Expand Down
12 changes: 12 additions & 0 deletions web/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ See [docs/reference/web-design-system.md](../docs/reference/web-design-system.md
- Confirmation / toasts -> `<ConfirmDialog>` / `<Toast>` (Zustand-backed queue, NOT Base UI's Toast).
- Cmd+K / shortcuts -> `<CommandPalette>` / `<KeyboardShortcutHint>` / `<CommandCheatsheet>`.
- Animation -> `<AnimatedPresence>` / `<StaggerGroup>` / `<LiveRegion>` (debounced ARIA live for WS updates).
- Icon helpers -> never write `getXIcon(value): LucideIcon` factories that return a component reference and get called inside JSX render bodies (the `react-x/static-components` rule flags them as "components created during render"). Export a `<XIcon value={...} {...lucideProps} />` wrapper component instead, doing the lookup inside the wrapper via `createElement` (avoids a PascalCase JSX binding in the wrapper body too). See `web/src/utils/activity-event-icon.tsx` and `web/src/pages/mcp-catalog/catalog-icons.tsx` for the canonical shape. Wrapper components live in their own file (NOT alongside utility exports) so React Fast Refresh stays compatible per the `react-refresh/only-export-components` rule.
- Viewport-size reads -> `useViewportSize()` from `@/hooks/useViewportSize` (`useSyncExternalStore` over `window` resize). Never read `window.innerWidth` / `window.innerHeight` directly inside a component render body or `useMemo`; the `react-x/globals` rule will flag it and it would be stale across resizes anyway.

## ESLint (MANDATORY)

`@eslint-react/eslint-plugin` v5+ via the `recommended-type-checked` preset (requires `parserOptions.projectService: true`, configured in `web/eslint.config.js`). Explicit error-level opt-ins beyond the preset:

- `@eslint-react/web-api-no-leaked-fetch`: detect `fetch()` in effects without `AbortController` cleanup.
- `@eslint-react/no-leaked-conditional-rendering`: catch the `{count && <Foo />}` bug where `0` renders verbatim. For `ReactNode | undefined` props use `{value != null && value !== false && <jsx>}`; for compound truthiness use `Boolean(...)`.
- `@eslint-react/globals`: restrict `window` / `document` / `localStorage` / etc. inside render. Hoist offenders into a `useCallback` event handler, a `useEffect`, or a `useSyncExternalStore`-backed hook.

Lint runs via `npm --prefix web run lint` with `--max-warnings 0`. To enumerate stale `eslint-disable` directives after a rule reshuffle: `npm --prefix web run lint -- --report-unused-disable-directives-severity=warn`.

## Base UI Adoption Decisions

Expand Down
36 changes: 33 additions & 3 deletions web/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,30 @@ import { reactRefresh } from 'eslint-plugin-react-refresh'
import pluginSecurity from 'eslint-plugin-security'

// TODO: Add eslint-plugin-react-hooks when it supports ESLint 10 (v5 caps at ESLint 9).
// @eslint-react provides some hooks analysis via hooks-extra rules in the meantime.
// @eslint-react provides hooks analysis via the recommended-type-checked preset
// in the meantime (rules-of-hooks, exhaustive-deps, set-state-in-effect, etc.).

export default tseslint.config(
{ ignores: ['dist/**'] },
js.configs.recommended,
...tseslint.configs.recommended,
eslintReact.configs['recommended-typescript'],
eslintReact.configs['recommended-type-checked'],
pluginSecurity.configs.recommended,
{
// Type-aware eslint-react rules (no-leaked-conditional-rendering,
// dom-no-unknown-property, no-unused-state, static-components, etc.)
// need the TypeScript project service so the rule implementations can
// read inferred types. ``projectService: true`` auto-discovers the
// nearest tsconfig per file so we don't have to enumerate the project
// graph by hand.
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
plugins: {
'react-refresh': reactRefresh.plugin,
Expand All @@ -35,7 +51,7 @@ export default tseslint.config(
// Rule flags every obj[var] with no data-flow analysis -- too many false
// positives. Prototype pollution is guarded explicitly at system boundaries.
'security/detect-object-injection': 'off',
// -- eslint-react rules not in recommended-typescript --
// -- eslint-react rules not in recommended-type-checked --
// Prevent dollar signs from leaking into rendered JSX output
'@eslint-react/jsx-no-leaked-dollar': 'error',
// Remove unnecessary <></> fragment wrappers
Expand All @@ -50,6 +66,20 @@ export default tseslint.config(
'@eslint-react/no-unstable-context-value': 'warn',
// Catch unstable default props that cause unnecessary re-renders
'@eslint-react/no-unstable-default-props': 'warn',
// -- v5 explicit opt-ins beyond the preset --
// Detect fetch() in effects without AbortController cleanup. We use
// axios via apiClient today, but the rule guards future raw fetch
// call sites and matches the pattern this plugin's other
// no-leaked-* rules cover.
'@eslint-react/web-api-no-leaked-fetch': 'error',
// Catch the {count && <Foo />} bug where a falsy 0 gets rendered
// verbatim instead of nothing. Type-aware -- requires projectService.
'@eslint-react/no-leaked-conditional-rendering': 'error',
// Restrict global var usage (window, document, localStorage, etc.)
// inside component render bodies. Hoist any new offenders into an
// event handler / useEffect / useSyncExternalStore-backed hook
// (see ``@/hooks/useViewportSize`` for the canonical pattern).
'@eslint-react/globals': 'error',
},
},
{
Expand Down
Loading
Loading