Guidance for any coding agent (Humans, Claude Code, Cursor, Codex, etc.). This is the single source of truth. CLAUDE.md imports it.
Percona UI (@percona/percona-ui) is a React + MUI v7 component library published as an npm package (themed components, design tokens, form inputs). Zero infrastructure: no DB/Docker/services needed to build, test, or run Storybook.
- Build:
pnpm build(Rollup →dist/) - Build watch:
pnpm build:watch - Storybook dev:
pnpm storybook:dev(port 6006, headless via--no-open) - Storybook build:
pnpm storybook:build - Type check:
pnpm typecheck(app +.storybooktsconfigs) - Lint:
pnpm lint(ESLint oversrc) - Format:
pnpm format(Prettier write) · check only:pnpm format:check - Test:
pnpm test(Vitest, run once) · watch:pnpm test:watch
Before considering the work done, run the safety gate and make it pass:
pnpm typecheck && pnpm lint && pnpm format:check && pnpm test
If format:check fails, fix it with pnpm format (don't hand-edit whitespace). If typecheck, lint, or test fail, fix the underlying issue and re-run the full chain until green. Do not report a change as complete until this passes.
Pre-existing and cosmetic — do not treat as failures:
pnpm build: a TS5096 warning (allowImportingTsExtensions) and a circular-dependency note.pnpm install: pnpm 10 reports "ignored build scripts" for esbuild; the binary still resolves correctly and doesn't block Vite/Storybook/Vitest.
- Themes — three variants (
base,pmm,sep) undersrc/design/themes/.getThemeOptions(themeName)is curried — call it asgetThemeOptions(name)(mode)(the return value is(mode) => ThemeOptions). PMM/SEP extend Base viamergeThemeOptions(src/design/merge-theme-options.ts). - ThemeContextProvider wraps MUI's
ThemeProviderwith a light/dark toggle (ColorModeContext), persisting mode to localStorage. - Components — follow the existing folder layout for new ones: component file +
.types.ts+.stories.tsx+index.tsbarrel, with both named and default exports. Form inputs (src/components/form/inputs/) integrate withreact-hook-form.
Every documented story carries exactly one maturity status on its meta (default export) tags, appended to built-in tags (autodocs/!dev) — never overwriting them.
- One source of truth:
.storybook/maturity-tags.tsdefines the closed set (stable,experimental,needs-review,deprecated), labels, descriptions, colors, andbadgeStyle. To add or restyle a status, edit this file only —main.ts(filter registration),manager.ts(sidebar/toolbar badges viastorybook-addon-tag-badges), andsrc/Introduction.mdx(homepage legend) all derive from it. - MDX docs & folders: tag a docs page with
<Meta tags={['stable']} />. A folder's badge is the intersection of its children's tags, so every direct child must share a status for the folder to show it. - Enforced:
src/maturity-tags.spec.tsfails if any*.stories.{ts,tsx}is missing a status or declares more than one.
React 18 + TypeScript strict (react-jsx transform). Rollup bundles to ESM with sourcemaps; peer deps (MUI, Emotion, React, react-hook-form) are externalized. Fonts via @fontsource (Poppins, Roboto, Roboto Mono).
@/utils→./src/utils(configured in tsconfig.json)
Barrel imports (@mui/material, @mui/icons-material) hurt dev startup/rebuild most, not just bundle size. Use default path imports (matches MUI docs).
- Components:
import Button from '@mui/material/Button'— notimport { Button } from '@mui/material'. - Icons:
import Delete from '@mui/icons-material/Delete'— not the barrel. - Migrate existing barrels:
npx @mui/codemod@latest v5.0.0/path-imports <path>. - Optional ESLint guard:
no-restricted-importswith{ "regex": "^@mui/[^/]+$" }blocks package-root barrels only. - VS Code nudge:
typescript.preferences.autoImportSpecifierExcludeRegexes: ["^@mui/[^/]+$"]in.vscode/settings.json.
- Child themes use
mergeThemeOptions, not rawdeepmerge. PMM/SEP extend BaseTheme viamergeThemeOptions(src/design/merge-theme-options.ts): it composesstyleOverrides(child wins per-key, base slots survive) and concatenatesvariantsbase-first (child cascades last). UsemergeThemeOptions(baseThemeOptions(mode), newOptions); never rawdeepmerge. Array slots other thanvariantsare still replaced — extendmergeThemeOptionsif a new array slot must compose. - MUI v7 drives hover backgrounds via CSS custom properties. E.g.
IconButton's hover bg isvar(--IconButton-hoverBg), set per-variant. Override by setting the CSS var on the right slot (e.g."--IconButton-hoverBg": theme.palette.action.hoverincolorSecondary), not"&:hover": { backgroundColor: ... }. Grep--{Component}-in MUI source first.
- Outlined uses
palette.[color].lightfor text + border — exceptwarning, which useswarning.main.warning.lightis a yellow/gold hue with insufficient contrast on light backgrounds;mainis the smallest step that holds. Don't normalize warning tolight— it's a perceptual constraint. - Filled uses the
palette.[color].surfacetoken (not MUI-standard). Don't refactorsurfaceto.light/.main— those are reserved for borders/icons and have a different perceptual role.
The <Box sx={{ mr: -1.75 }} /> rendered when no icon is passed is intentional — it preserves text start-line alignment with icon-bearing rows.
- Tooltips render in the inverted app mode (light app → dark tooltip surface, dark app → light tooltip surface). Don't add per-theme tooltip overrides — change
BaseThemeinstead where possible. - Link styling inside tooltips lives as a nested
& .MuiLink-rootrule inside the tooltip override, not inMuiLink.styleOverrides. Don't move link styles back toMuiLink.
Two parallel layers, intentionally distinct — don't collapse them:
palette.action.{active,hover,selected,disabled,focus}= neutral state tints, brand-black-tinted in all themes; used wherever the surface is default-colored.palette.primary.{hover,selected,focus,focusVisible,outlinedBorder}= state tints for primary-colored surfaces. Identical toaction.*in Base (both brand-black) but diverges in PMM/SEP (purple).- Per-mode opacity is mandatory: dark mode uses higher alpha (8% hover vs 4% light, 16% selected vs 8% light, 15% disabled/focus vs 12% light), wired through
tokens.action.{hoverOpacity,selectedOpacity,disabledOpacity,focusOpacity}so MUI'salpha(...)calculations stay in sync.
PMM and SEP extend BaseTheme with mergeThemeOptions (see MUI Conventions). Child themes may override any styleOverrides slot and add variants — both compose with Base, so baseline styles are always respected and the child wins only on genuine conflicts. Shared behavior belongs in BaseTheme; brand-specific tweaks belong in PMM/SEP.
- Prefer no comment; code should be self-explanatory. One short line where a comment is genuinely needed — never paragraph-style docblocks for obvious logic.
- If a comment needs more than ~150 characters, refactor or move the explanation to the PR/commit — not source.
- Comment only non-obvious caveats worth flagging. Applies to
//and/* */in TypeScript/TSX.