关于 prowler-ui
prowler-ui 是面向 Prowler UI 开发的编码技能,提供组件、样式、目录结构与代码质量方面的约定,帮助开发者遵循现有设计系统和项目模式。
主要功能
- 01shadcn/ui 与 Tailwind 使用规范
- 02组件放置与目录决策规则
- 03表单验证与状态管理模式
- 04共享样式和显示工具复用
- 05提交前质量检查清单
适用场景
- 01为 Prowler UI 添加页面与组件
- 02按既有模式实现表单和状态
- 03审查界面变更是否遵循设计系统
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ prowler-cloud/ prowler --skill prowler-ui安装后在 Claude Code 中输入/prowler-ui确认已加载。
附带文件references/ui-docs.md
详细介绍
相关通用技能
typescript- Const 类型、扁平接口react-19- 不使用 useMemo/useCallback、编译器nextjs-16- App Router、Server Actionstailwind-4- cn() 工具、样式规则zod-4- Schema 验证zustand-5- 状态管理ai-sdk-5- 聊天/AI 功能playwright- E2E 测试(另见prowler-test-ui)
技术栈(版本)
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
关键:组件库规则
- 始终:使用
shadcn/ui+ Tailwind(components/shadcn/) - 绝不:向
components/ui/添加组件(仅为 prowler-cloud 覆盖层提供临时重新导出垫片)
设计系统规范(必须遵守)
适用于所有 UI 工作。设计系统是唯一可信来源——准确复用,并审慎扩展。
- 优先复用,绝不重新发明。 开始构建前,先在
components/shadcn/和代码库中的现有用法里查找等效实现。已有组件时,切勿创建自定义组件、模态框包装器或基础组件。 - 严格使用已定义的变体/样式,不多不少。 在调用处,通过组件的
variant/size/tone属性控制外观。绝不要为共享控件(Button、SelectTrigger、SelectItem、Modal、徽章等)添加临时视觉className(颜色、不透明度、悬停/聚焦/禁用状态、仅为视觉效果设置的间距),也绝不要跳过正确的语义变体。 - 模态框:仅使用
@/components/shadcn/modal。Select:使用components/shadcn/select。 - 颜色:复用
ui/styles/globals.css中现有的语义令牌。不要使用原始 Tailwind 颜色工具类(例如bg-blue-950/40),也不要使用十六进制颜色。如果没有合适的令牌,立即停止并询问设计负责人——不要自行发明或创建近似令牌。 - 确实需要新变体/令牌? 这属于设计系统变更:先将其添加到共享组件 API(并获得设计审批),再使用。绝不能在调用处自行决定。
审查 UI PR 时,标记以下问题:重复实现 shadcn 功能的自定义模态框/基础组件、在调用处为共享控件设置视觉 className、原始颜色工具类,以及未通过共享组件 API 引入的新变体/令牌。
决策树
组件放置位置
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
代码位置
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}/
已弃用:
components/ui/是一个临时重新导出垫片,用于为 prowler-cloud 覆盖层将旧版导入路径映射到components/shadcn/。 HeroUI 已完全移除。绝不要在此处添加或导入组件——请改用@/components/shadcn(基础组件)或@/components/{domain}。 待 cloud 仓库迁移到@/components/shadcn后,删除此垫片。
样式决策
Tailwind class exists? → className
Dynamic value? → style prop
Conditional styles? → cn()
Static only? → className (no cn())
Recharts/library? → CHART_COLORS constant + var()
作用范围规则(绝对)
- 在 2 个或更多位置使用 →
lib/或types/或hooks/(组件放在components/{domain}/) - 在 1 个位置使用 → 保留在功能目录中
- 所有文件夹结构决策均由此规则决定
项目结构
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(特殊情况)
对于不接受 className 的 Recharts 属性:
const CHART_COLORS = {
primary: "var(--color-primary)",
secondary: "var(--color-secondary)",
text: "var(--color-text)",
gridLine: "var(--color-border)",
};
// Only use var() for library props, NEVER in className
<XAxis tick={{ fill: CHART_COLORS.text }} />
<CartesianGrid stroke={CHART_COLORS.gridLine} />
表单 + 验证模式
"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(), // Zod 4 syntax
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>
);
}
命令
# 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
批量与即时组件 API(必须遵守)
当组件同时支持批量(延迟、基于提交)和即时(立即回调)行为时,使用判别联合类型对两者的关联关系建模——绝不能将其定义为互不相关的可选属性。关联属性必须要么全部提供,要么全部不提供。
// ❌ 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;
这样,无效的属性组合会在编译时报错,而不会在运行时造成意外。
优先复用共享展示工具(必须遵守)
添加本地展示映射(标签、提供方名称、状态字符串、类别格式化函数)之前,先在 ui/types/* 和 ui/lib/* 中查找现有辅助工具。
// ✅ 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";
如果不存在辅助工具,且会在 2 个或更多位置使用,请将其添加到 ui/lib/ 或 ui/types/ 并复用。仅在恰好用于一个位置时保留为本地实现。
派生状态规则(必需)
避免使用 useState + useEffect 模式来同步 props 或 searchParams——这会导致同步错误和不必要的重新渲染。应直接从真实数据源派生值。
// ❌ 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
如果确实需要本地状态(例如乐观 UI,或提交前的待处理编辑),请添加简短注释:
// Local state needed: user edits are buffered until "Apply" is clicked
const [pending, setPending] = useState(initialValues);
标签映射的严格键类型(必需)
已知键集合时,避免使用 Record<string, string>。请使用显式联合类型或 const 键对象,以便在编译时捕获拼写错误。
// ❌ Loose — typos compile silently
const STATUS_LABELS: Record<string, string> = {
actve: "Active", // typo, no error
};
// ✅ Tight — union key
type Status = "active" | "inactive" | "pending";
const STATUS_LABELS: Record<Status, string> = {
active: "Active",
inactive: "Inactive",
pending: "Pending",
// actve: "Active" ← compile error
};
// ✅ Also fine — const satisfies
const STATUS_LABELS = {
active: "Active",
inactive: "Inactive",
pending: "Pending",
} as const satisfies Record<Status, string>;
提交前 QA 检查清单
-
pnpm run typecheck通过 -
pnpm run lint:fix通过 -
pnpm run format:write通过 - 相关 E2E 测试通过
- 已处理所有 UI 状态(加载、错误、空状态)
- 代码中没有密钥(使用
.env.local) - 错误消息已清理(不向用户显示堆栈跟踪)
- 存在服务端验证(不要信任客户端)
- 无障碍:键盘导航、ARIA 标签
- 适配移动设备(如适用)
重新请求审查前的检查清单(审查讨论串规范)
在请求审查者重新审查之前:
- 每个未解决的行内讨论串都已修复,或已明确回复并说明理由
- 如果你同意某条评论:更改已提交,并在回复中注明提交哈希
- 如果你不同意:回复中应清楚说明理由——不要让讨论串一直处于未解决状态
- 仅在所有讨论串都已妥善处理后,才重新请求审查
资源
- 文档:请参阅 references/ 获取本地开发指南的链接