원클릭으로
client-componentization
Use when adding, changing, extracting, or reusing client components, including Storybook stories and controls.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when adding, changing, extracting, or reusing client components, including Storybook stories and controls.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when adding or changing forms in apps/client.
Use when changing GitHub OAuth, session cookies, SessionGuard, logout, or authenticated API request identity.
Use when adding or changing routes in apps/client.
Use when adding or changing client state, React Query data flow, immutable context values, or Zustand stores.
Use when styling apps/client with Tailwind CSS v4.
Use when adding or changing client API query or mutation hooks.
| name | client-componentization |
| description | Use when adding, changing, extracting, or reusing client components, including Storybook stories and controls. |
Generated from ai/registry.json. Do not edit manually.
Canonical skill: ../../../ai/skills/client-componentization.md.
Referenced context:
../../../ai/rules/client-component-rules.md../../../ai/rules/client-state-rules.md../../../ai/rules/client-form-rules.md../../../ai/architecture/client-app.md../../../ai/examples/good-client-component.md../../../ai/examples/good-client-form.mdThis file is compiled from canonical AI knowledge files. Edit canonical files under ai, then run npm run ai:sync.
ai/skills/client-componentization.mdUse this skill when adding, changing, extracting, or reusing React components, repeated UI, large components, route-level screens, or form-heavy UI in apps/client.
Split UI by real ownership and reuse without creating a broad component library or hiding state in god hooks/providers.
ai/rules/client-component-rules.mdai/rules/client-state-rules.mdai/rules/client-form-rules.mdai/architecture/client-app.mdai/examples/good-client-component.mdai/examples/good-client-form.mdsrc/components, page-specific pieces under the owning page folder, and layout-specific pieces under the owning layout folder.stories/ child folder that matches the component's source area.src/stories for route, layout, and page stories.stories/ folder layout instead of loose sibling *.stories.tsx files.args and argTypes stay in sync with declared component props.npm --workspace @capture-flag/client run storybook:build after Storybook, component, or story changes.npm --workspace @capture-flag/client run build.ai/rules/client-component-rules.mdRules for React component boundaries in apps/client.
src/components.src/core/<category>/<name>.ts, with one exported function or hook per file.src/layouts/<LayoutName>.src/pages.src/pages/<PageName> when they are not shared outside that page.src/core utilities and hooks from their direct alias file path such as @core/json/formatJson; do not add index.ts barrels under src/core.@components/Button; do not assume a central src/components/index.ts barrel exists.apps/client at or below 400 lines; when touching larger existing files, prefer splitting real UI responsibilities instead of expanding them further.children for layout wrappers such as cards, shells, and empty states.className, aria-invalid, and ref.apps/client.stories/ child folder next to the component folder they cover, using *.stories.tsx; route/panel grouping stories belong in the owning route folder's stories/ folder.src/components/stories and member component stories in src/components/members/stories.src/layouts/<LayoutName>/stories.src/pages/<PageName>/stories or src/pages/<PageName>/<section>/stories.src/pages/stories.src/stories; do not put component stories there.src/core.args and argTypes updates.*.stories.tsx beside component files when a nearby stories/ folder is available..map() when rendering API data, dynamic collections, long repeated groups, or lists whose members are not all known at author time.src/core/date, src/core/json, src/core/strings, src/core/validation, or src/core/hooks only for helpers that are independent of Capture Flag domain context.src/core/<category>/__tests__/<name>.test.ts.@core/json/formatJson over barrels or grouped core imports.*.stories.tsx file per component or cohesive route grouping.src/stories/mockData.ts when stories need realistic Capture Flag data..storybook/preview.tsx for route and panel stories that call client API hooks.parameters.router.initialEntries when a story depends on React Router params.parameters.layout = "fullscreen" for route, layout, shell, and panel stories that need app-width context.npm --workspace @capture-flag/client run storybook:build after Storybook, component, or story changes.npm --workspace @capture-flag/client run build after component moves.ai/rules/client-state-rules.mdRules for state ownership in apps/client.
queryClient.invalidateQueries.useRouteContext to derive selected organization, project, config, and environment from route params, search params, and React Query data.useCollectionSelection for page-local collection selection that does not need to be linkable.src/core/hooks/<hook>.ts and import them directly from that file.src/api/<domain> for server/cache state.useRouteContext for selected route resources derived from navigation state plus server data.useState or React Hook Form state for component-only state.src/core/hooks/<hook>.ts only for repeated hooks that are independent of page/domain context.ai/rules/client-form-rules.mdRules for forms in apps/client.
react-hook-form for mutation forms and forms with meaningful client validation.zod for form schemas when a form submits data to API mutations or has reusable validation rules.zodResolver from @hookform/resolvers/zod when using React Hook Form with Zod.defaultValues for every registered React Hook Form field.noValidate on forms so Zod owns validation messages."".aria-invalid on invalid fields.FormField in complex forms when they reduce repeated label/control/error markup.FormData manually in React components when React Hook Form or controlled state owns the form.required validation for app-level messages.ai/architecture/client-app.mdapps/client is a Vite React application for the Capture Flag platform UI.
src/main.tsx owns top-level providers.src/router.tsx owns React Router route definitions.lazyRoute() helper and should expose named exports.src/layouts contains route layout wrappers that render shared shells, navigation, headers, and nested <Outlet /> regions.src/pages contains route-level screens. Multi-file route screens use folder modules with index.ts named exports; simple one-file screens may stay as direct page files.src/components contains shared UI used by multiple pages or sections.src/core contains context-independent client utilities and reusable hooks organized by category.src/api contains client request functions, React Query hooks, operation barrels, domain barrels, and domain query keys.src/routing contains route path and route context helpers shared by pages and layouts.src/stories contains shared Storybook fixtures and API mocks, not component stories.src/test contains shared Vitest and Testing Library helpers.PlatformLayout owns the authenticated shell, navigation frame, sidebar/header state, logout flow, and nested route outlet.useRouteContext, not stored in a mutable layout context./: redirects to /organizations./login: GitHub login screen./account: authenticated user account details and display name editing./organizations and /organizations/:organizationId: organization selection and organization members./organizations/:organizationId/projects and /organizations/:organizationId/projects/:projectId: project selection and project members./organizations/:organizationId/projects/:projectId/environments: environments for the selected project./organizations/:organizationId/projects/:projectId/configs and /organizations/:organizationId/projects/:projectId/configs/:configId: configs and public Config JSON preview./organizations/:organizationId/projects/:projectId/configs/:configId/flags: feature flags and remote config values./organizations/:organizationId/projects/:projectId/configs/:configId/segments: reusable targeting segments./organizations/:organizationId/projects/:projectId/sdk-keys: SDK key lifecycle for project configs/environments, with selected config/environment in ?configId= and ?environmentId= when needed./organizations/:organizationId/audit-logs: organization/project audit log timeline, with selected project in ?projectId= when needed.*: redirects to /.src/api/<domain>/<operation>.useQuery or useInfiniteQuery according to the API shape.useRouteContext for selected resources and redirect-safe navigation paths.src/layouts/<LayoutName> until reused by another layout or page.src/components and are imported directly through aliases such as @components/Button.components/members primitives with page-specific role options.stories/ child folder beside the source area they cover.src/pages/stories.src/stories and are reused by stories and page tests.src/core/<category>/<name>.ts.date, json, strings, validation, and hooks.src/core/date/toDate.index.ts barrels under src/core; multiple helpers require multiple explicit imports.src/core/<category>/__tests__/<name>.test.ts next to the category they cover.__tests__/ child folders beside the source area they cover, mirroring Storybook stories/ folders.src/test and provide Testing Library render helpers, React Query providers, and fetch response mocks.npm --workspace @capture-flag/client run test:coverage and should stay at 90% or higher for configured client targets.src/api/__tests__ and cover operation behavior through mocked fetch responses.src/components/__tests__; member component tests live in src/components/members/__tests__.src/pages/<PageName>/__tests__ when they cover one page folder.src/pages/<PageName>/<section>/__tests__ when they cover colocated page internals such as feature flags or segments.src/__tests__ only for source files owned directly by src, such as permissions.ts.src/components/stories; member component stories live in src/components/members/stories.src/layouts/<LayoutName>/stories.stories/ child folder.src/pages/stories and use route parameters plus shared mock data to render realistic page states.src/stories; that folder is reserved for shared Storybook data, routes, and API mocks.ai/examples/good-client-component.mdSource: apps/client/src/components/Button.tsx (sha256: 7e1bcb1c45c5d1a5610f108be5028ce68d34ded2c394d326428eca08702a992d)
Source: apps/client/src/components/Panel.tsx (sha256: c0dfcd9d6984e9741af7abe91c60ad8bcb30e4d8f18c3c1456bdea5882d2472f)
Why this is canonical:
classnames as cls for optional classes.Canonical shared component patterns from apps/client/src/components.
import cls from "classnames";
import type { ComponentPropsWithoutRef } from "react";
type ButtonVariant = "primary" | "secondary" | "danger" | "ghost";
const baseButtonClassName =
"inline-flex h-9 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium shadow-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
const buttonClassNames: Record<ButtonVariant, string> = {
danger:
"border border-transparent bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20",
ghost:
"border border-transparent bg-transparent text-foreground shadow-none hover:bg-accent hover:text-accent-foreground",
primary: "border border-transparent bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
};
type ButtonProps = ComponentPropsWithoutRef<"button"> & {
variant?: ButtonVariant;
};
export function Button({ className, variant = "primary", ...props }: ButtonProps) {
return (
<button className={cls(baseButtonClassName, buttonClassNames[variant], className)} {...props} />
);
}
This accepts native button props, keeps variants explicit, and uses cls for optional classes.
export function Panel({ children, className, showTitle = true, title, wide = false }: PanelProps) {
return (
<section
className={cls("grid gap-4 text-foreground", className, {
"lg:col-span-2": wide,
})}
>
{showTitle ? <h2 className="text-xl font-semibold tracking-tight">{title}</h2> : null}
{children}
</section>
);
}
This keeps layout composition explicit through children, accepts optional native composition through className, and preserves product visual language.
ai/examples/good-client-form.mdSource: apps/client/src/components/CreateNameForm.tsx (sha256: e60e8092b9d41bd7a2b5136ff048d6b5455abe40c796386dea079369b823e1e3)
Source: apps/client/src/pages/FlagsPage/featureFlags/CreateFeatureFlagForm.tsx (sha256: a7d1e86cdb9b5082b43efe4959ecae33eb6eeda011e2a78d8df8fc7ed8a76b02)
Why this is canonical:
zodResolver and noValidate.FormField wrapper in complex forms to reduce repeated label/control/error markup.Canonical form pattern from apps/client/src/components/CreateNameForm.tsx.
const createNameFormSchema = z.object({
name: z.string().trim().min(1, "Enter a name.").max(120, "Use up to 120 characters."),
});
type CreateNameFormValues = z.infer<typeof createNameFormSchema>;
const {
formState: { errors, isSubmitting },
handleSubmit,
register,
reset,
} = useForm<CreateNameFormValues>({
defaultValues: {
name: "",
},
resolver: zodResolver(createNameFormSchema),
});
<form
className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start"
noValidate
onSubmit={handleSubmit(submit)}
>
<div>
<TextInput
aria-invalid={errors.name ? true : undefined}
disabled={isDisabled}
placeholder={placeholder}
{...register("name")}
/>
<FieldError>{errors.name?.message}</FieldError>
</div>
<Button className="justify-self-start" disabled={isDisabled} type="submit">
Create
</Button>
</form>
This pattern keeps schema, default values, noValidate, field errors, and submit reset close to the owning form.
<FormField error={errors.key?.message} label="SDK key" required htmlFor={keyId}>
<TextInput
aria-invalid={errors.key ? true : undefined}
autoComplete="off"
disabled={isDisabled}
id={keyId}
placeholder="newCheckout"
{...register("key")}
/>
</FormField>
function FormField({ children, error, htmlFor, label, required = false }: FormFieldProps) {
return (
<div className="grid gap-2">
<label className="text-sm font-medium text-foreground" htmlFor={htmlFor}>
{label}
{required ? <span className="text-destructive"> *</span> : null}
</label>
{children}
<FieldError>{error}</FieldError>
</div>
);
}
This pattern is local to the owning form and avoids promoting page-specific field structure into shared components too early.