ALWAYS: Use shadcn/ui + Tailwind (components/shadcn/)
NEVER: Add components to components/ui/ (temporary re-export shims for the prowler-cloud overlay only)
Design System Discipline (REQUIRED)
Applies to ALL UI work. The design system is the single source of truth — reuse it exactly, extend it deliberately.
Reuse first, never reinvent. Before building anything, search components/shadcn/ and existing usages in the codebase for an equivalent. Do NOT create a custom component, modal wrapper, or primitive when one already exists.
Use exactly the defined variants/styles — no more, no less. At the call site, drive appearance through the component's variant/size/tone props. Never add ad-hoc visual className (color, opacity, hover/focus/disabled, spacing-for-looks) to shared controls (Button, SelectTrigger, SelectItem, Modal, badges…), and never skip the correct semantic variant.
Modals: only @/components/shadcn/modal. Selects: components/shadcn/select.
Colors: reuse existing semantic tokens from ui/styles/globals.css. No raw Tailwind color utilities (e.g. bg-blue-950/40), no hex. If no token fits, STOP and ask the design owner — do not invent or near-duplicate tokens.
Need a genuinely new variant/token? That is a design-system change: add it to the shared component API (with design sign-off), then consume it. It is never a call-site decision.
When reviewing UI PRs, flag: custom modals/primitives that duplicate shadcn, call-site visual className on shared controls, raw color utilities, and new variants/tokens introduced without going through the shared component API.
DECISION TREES
Component Placement
New UI primitive? → components/shadcn/ (shadcn/ui + Tailwind)
Used by 1 domain? → components/{domain}/
Used by 2+ domains? → components/shared/
Needs state/hooks? → "use client"
Server component? → No directive needed
Deprecated:components/ui/ is a temporary re-export shim that maps
legacy import paths to components/shadcn/ for the prowler-cloud overlay.
HeroUI is fully removed. Never add or import components here — use
@/components/shadcn (primitives) or @/components/{domain} instead.
Delete the shim once the cloud repo migrates to @/components/shadcn.
# Development
cd ui && pnpm install
cd ui && pnpm run dev
# Code Quality
cd ui && pnpm run typecheck
cd ui && pnpm run lint:fix
cd ui && pnpm run format:write
cd ui && pnpm run healthcheck # typecheck + lint
# Testing
cd ui && pnpm run test:e2e
cd ui && pnpm run test:e2e:ui
cd ui && pnpm run test:e2e:debug
# Build
cd ui && pnpm run build
cd ui && pnpm start
Batch vs Instant Component API (REQUIRED)
When a component supports both batch (deferred, submit-based) and instant (immediate callback) behavior, model the coupling with a discriminated union — never as independent optionals. Coupled props must be all-or-nothing.
// ❌ NEVER: Independent optionals — allows invalid half-states
interface FilterProps {
onBatchApply?: (values: string[]) => void;
onInstantChange?: (value: string) => void;
isBatchMode?: boolean;
}
// ✅ ALWAYS: Discriminated union — one valid shape per mode
type BatchProps = {
mode: "batch";
onApply: (values: string[]) => void;
onCancel: () => void;
};
type InstantProps = {
mode: "instant";
onChange: (value: string) => void;
// onApply/onCancel are forbidden here via structural exclusion
onApply?: never;
onCancel?: never;
};
type FilterProps = BatchProps | InstantProps;
This makes invalid prop combinations a compile error, not a runtime surprise.
Reuse Shared Display Utilities First (REQUIRED)
Before adding local display maps (labels, provider names, status strings, category formatters), search ui/types/* and ui/lib/* for existing helpers.
// ✅ CHECK THESE FIRST before creating a new map:
// ui/lib/utils.ts → general formatters
// ui/types/providers.ts → provider display names, icons
// ui/types/findings.ts → severity/status display maps
// ui/types/compliance.ts → category/group formatters
// ❌ NEVER add a local map that already exists:
const SEVERITY_LABELS: Record<string, string> = {
critical: "Critical",
high: "High",
// ...duplicating an existing shared map
};
// ✅ Import and reuse instead:
import { severityLabel } from "@/types/findings";
If a helper doesn't exist and will be used in 2+ places, add it to ui/lib/ or ui/types/ and reuse it. Keep local only if used in exactly one place.
Derived State Rule (REQUIRED)
Avoid useState + useEffect patterns that mirror props or searchParams — they create sync bugs and unnecessary re-renders. Derive values directly from the source of truth.
// ❌ NEVER: Mirror props into state via effect
const [localFilter, setLocalFilter] = useState(filter);
useEffect(() => { setLocalFilter(filter); }, [filter]);
// ✅ ALWAYS: Derive directly
const localFilter = filter; // or compute inline
If local state is genuinely needed (e.g., optimistic UI, pending edits before submit), add a short comment:
// Local state needed: user edits are buffered until "Apply" is clicked
const [pending, setPending] = useState(initialValues);
Strict Key Typing for Label Maps (REQUIRED)
Avoid Record<string, string> when the key set is known. Use an explicit union type or a const-key object so typos are caught at compile time.