| name | prowler-ui |
| description | Prowler UI-specific patterns. For generic patterns, see: typescript, react-19, nextjs-16, tailwind-4. Trigger: When working inside ui/ on Prowler-specific conventions (shadcn, folder placement, actions/adapters, shared types/hooks/lib).
|
| license | Apache-2.0 |
| metadata | {"author":"prowler-cloud","version":"1.1","scope":["root","ui"],"auto_invoke":["Creating/modifying Prowler UI components","Reviewing Prowler UI components","Working on Prowler UI structure (actions/adapters/types/hooks)"]} |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task |
Related Generic Skills
typescript - Const types, flat interfaces
react-19 - No useMemo/useCallback, compiler
nextjs-16 - App Router, Server Actions
tailwind-4 - cn() utility, styling rules
zod-4 - Schema validation
zustand-5 - State management
ai-sdk-5 - Chat/AI features
playwright - E2E testing (see also prowler-test-ui)
Tech Stack (Versions)
Next.js 16.2.3 | React 19.2.5 | Tailwind 4.1.18 | shadcn/ui
Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8
NextAuth 5.0.0-beta.30 | Recharts 2.15.4
CRITICAL: Component Library Rule
- 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
Code Location
Server action → actions/{feature}/{feature}.ts
Data transform → actions/{feature}/{feature}.adapter.ts
Types (shared 2+) → types/{domain}.ts
Types (local 1) → {feature}/types.ts
Utils (shared 2+) → lib/
Utils (local 1) → {feature}/utils/
Hooks (shared 2+) → hooks/
Hooks (local 1) → {feature}/hooks.ts
UI primitive → components/shadcn/
Domain component → components/{domain}/
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.
Styling Decision
Tailwind class exists? → className
Dynamic value? → style prop
Conditional styles? → cn()
Static only? → className (no cn())
Recharts/library? → CHART_COLORS constant + var()
Scope Rule (ABSOLUTE)
- Used 2+ places →
lib/ or types/ or hooks/ (components go in components/{domain}/)
- Used 1 place → keep local in feature directory
- This determines ALL folder structure decisions
Project Structure
ui/
├── app/
│ ├── (auth)/ # Auth pages (login, signup)
│ └── (prowler)/ # Main app
│ ├── compliance/
│ ├── findings/
│ ├── providers/
│ ├── scans/
│ ├── services/
│ └── integrations/
├── components/
│ ├── shadcn/ # shadcn/ui primitives (USE THIS)
│ ├── shared/ # Cross-domain composed components (2+ domains)
│ ├── ui/ # DEPRECATED shim → re-exports shadcn (do not use)
│ ├── {domain}/ # Domain-specific (compliance, findings, providers, etc.)
│ ├── filters/ # Filter components
│ ├── graphs/ # Chart components
│ └── icons/ # Icon components
├── actions/ # Server actions
├── types/ # Shared types
├── hooks/ # Shared hooks
├── lib/ # Utilities
├── store/ # Zustand state
├── tests/ # Playwright E2E
└── styles/ # Global CSS
Recharts (Special Case)
For Recharts props that don't accept className:
const CHART_COLORS = {
primary: "var(--color-primary)",
secondary: "var(--color-secondary)",
text: "var(--color-text)",
gridLine: "var(--color-border)",
};
<XAxis tick={{ fill: CHART_COLORS.text }} />
<CartesianGrid stroke={CHART_COLORS.gridLine} />
Form + Validation Pattern
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
email: z.email(),
name: z.string().min(1),
});
type FormData = z.infer<typeof schema>;
export function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
await serverAction(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
Commands
cd ui && pnpm install
cd ui && pnpm run dev
cd ui && pnpm run typecheck
cd ui && pnpm run lint:fix
cd ui && pnpm run format:write
cd ui && pnpm run healthcheck
cd ui && pnpm run test:e2e
cd ui && pnpm run test:e2e:ui
cd ui && pnpm run test:e2e:debug
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.
interface FilterProps {
onBatchApply?: (values: string[]) => void;
onInstantChange?: (value: string) => void;
isBatchMode?: boolean;
}
type BatchProps = {
mode: "batch";
onApply: (values: string[]) => void;
onCancel: () => void;
};
type InstantProps = {
mode: "instant";
onChange: (value: string) => void;
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.
const SEVERITY_LABELS: Record<string, string> = {
critical: "Critical",
high: "High",
};
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.
const [localFilter, setLocalFilter] = useState(filter);
useEffect(() => { setLocalFilter(filter); }, [filter]);
const localFilter = filter;
If local state is genuinely needed (e.g., optimistic UI, pending edits before submit), add a short comment:
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.
const STATUS_LABELS: Record<string, string> = {
actve: "Active",
};
type Status = "active" | "inactive" | "pending";
const STATUS_LABELS: Record<Status, string> = {
active: "Active",
inactive: "Inactive",
pending: "Pending",
};
const STATUS_LABELS = {
active: "Active",
inactive: "Inactive",
pending: "Pending",
} as const satisfies Record<Status, string>;
QA Checklist Before Commit
Pre-Re-Review Checklist (Review Thread Hygiene)
Before requesting re-review from a reviewer:
Resources
- Documentation: See references/ for links to local developer guide