Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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