| name | client-tailwind-v4 |
| description | Use when styling apps/client with Tailwind CSS v4. |
Generated from ai/registry.json. Do not edit manually.
Canonical skill: ../../../ai/skills/client-tailwind-v4.md.
Referenced context:
../../../ai/rules/client-styling-rules.md
../../../ai/rules/client-component-rules.md
../../../ai/architecture/client-app.md
../../../ai/examples/good-client-component.md
This file is compiled from canonical AI knowledge files. Edit canonical files under ai, then run npm run ai:sync.
Compiled AI Skill: client-tailwind-v4
Canonical Skill: ai/skills/client-tailwind-v4.md
Client Tailwind CSS v4
Use this skill when styling apps/client with Tailwind CSS v4.
Goal
Preserve the existing product visual language while using Tailwind utilities and responsive behavior correctly.
Read First
ai/rules/client-styling-rules.md
ai/rules/client-component-rules.md
ai/architecture/client-app.md
ai/examples/good-client-component.md
Workflow
- Inspect nearby components for existing spacing, color, border, and state patterns.
- Keep global CSS limited to Tailwind import and small base styles.
- Put layout and visual treatment in component
className utilities.
- Extract repeated visual patterns into focused components when repetition is real.
- Check desktop and mobile layouts before finishing.
Expected Output
- No new Tailwind config unless a concrete design primitive requires it.
- No broad design-system abstraction unless existing repetition justifies it.
- Styling remains semantic HTML plus Tailwind utilities.
Verification
- Run
npm --workspace @capture-flag/client run build.
- Check mobile breakpoints for changed grids, forms, and tables when feasible.
Referenced Context
Reference: ai/rules/client-styling-rules.md
Client Styling Rules
Rules for Tailwind CSS v4 and class composition in apps/client.
Always
- Use Tailwind CSS v4 through
@tailwindcss/vite and @import "tailwindcss".
- Keep global CSS limited to Tailwind import, Tailwind v4 theme tokens, dark-mode custom variants, document defaults, and small base styles.
- Define shared theme primitives in
styles.css with CSS custom properties and @theme inline when they are true app-wide tokens.
- Prefer utility classes in components over custom CSS selectors.
- Preserve responsive behavior on desktop and mobile.
- Use the
classnames package for conditional class composition.
- Import
classnames as cls with import cls from "classnames";.
- Use the object form for conditional classes:
cls("base", { "active": isActive }).
- Simple full-class replacement ternaries are acceptable for one-off binary variants; use
cls when appending or composing conditional class sets.
- Keep static Tailwind class sets as plain strings when there are no conditions.
Never
- Do not add a Tailwind config file unless custom theme primitives cannot stay local.
- Do not add local
classNames, cn, or wrapper helpers without a concrete repeated need.
- Do not use template literals only to append conditional classes.
- Do not add broad selectors to
styles.css when component utility classes can express the behavior.
- Do not turn repeated class sets into broad design-system abstractions before reuse is real.
- Do not create generic, interchangeable layouts when the existing product tone should be preserved.
Verification
- Search changed files for local class-name helpers before finishing.
- Run
npm --workspace @capture-flag/client run build after styling changes.
Reference: ai/rules/client-component-rules.md
Client Component Rules
Rules for React component boundaries in apps/client.
Always
- Extract components when UI repeats or a named component makes screen composition clearer.
- Keep shared client components in
src/components.
- Keep context-independent utilities and reusable hooks in
src/core/<category>/<name>.ts, with one exported function or hook per file.
- Keep route layouts that wrap nested routes in
src/layouts/<LayoutName>.
- Keep route-level screens in
src/pages.
- Keep page-specific components and hooks under
src/pages/<PageName> when they are not shared outside that page.
- Import
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.
- Import shared components directly through aliases such as
@components/Button; do not assume a central src/components/index.ts barrel exists.
- Keep new or substantially changed React component files in
apps/client at or below 400 lines; when touching larger existing files, prefer splitting real UI responsibilities instead of expanding them further.
- Keep component props small and explicit.
- Prefer
children for layout wrappers such as cards, shells, and empty states.
- Prefer explicit JSX over array-driven rendering for a small, fixed set of known UI items.
- Extract custom hooks for repeated or stateful UI behavior.
- Turn repeated form field label/control/hint/error markup into small primitives before copying it again.
- Shared form controls must accept native props, extra
className, aria-invalid, and ref.
- Add or update Storybook stories when adding or changing reusable, layout, page-specific, or route-level React components in
apps/client.
- Keep Storybook stories in a
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.
- Add Storybook controls or actions for every public prop explicitly declared by the component; use controls for data props and actions for callbacks.
- Keep shared component stories in
src/components/stories and member component stories in src/components/members/stories.
- Keep layout stories in
src/layouts/<LayoutName>/stories.
- Keep page or page-section stories in
src/pages/<PageName>/stories or src/pages/<PageName>/<section>/stories.
- Keep cross-page route or panel grouping stories in
src/pages/stories.
- Keep Storybook fixtures, route constants, and API mocks in
src/stories; do not put component stories there.
Never
- Do not extract one-off UI when it adds indirection without reuse, naming clarity, or state-boundary value.
- Do not turn every extraction into a broad component library.
- Do not move page, domain, route, or API-specific helpers into
src/core.
- Do not let route components become god components.
- Do not fix a god component by moving all state and effects into a god provider or god hook.
- Do not build artificial arrays just to render a handful of fixed, known navigation or action items.
- Do not use React Context for mutable UI state.
- Do not copy fetched React Query data into component state just to pass it down.
- Do not leave component prop changes without matching Storybook
args and argTypes updates.
- Do not place
*.stories.tsx beside component files when a nearby stories/ folder is available.
Data-Driven Rendering
- Use arrays and
.map() when rendering API data, dynamic collections, long repeated groups, or lists whose members are not all known at author time.
- Render items directly when the UI is a short, fixed set of known product actions or navigation entries.
- Split large files by ownership such as layout shell, sidebar, selectors, form, list, detail, and helper hooks; do not hide a large component behind a single large hook.
Core Utilities
- Use
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.
- Keep tests for each core helper in
src/core/<category>/__tests__/<name>.test.ts.
- Prefer direct alias imports such as
@core/json/formatJson over barrels or grouped core imports.
Storybook Layout
- Use one
*.stories.tsx file per component or cohesive route grouping.
- Use shared mock entities and route strings from
src/stories/mockData.ts when stories need realistic Capture Flag data.
- Use the Storybook API mock installed by
.storybook/preview.tsx for route and panel stories that call client API hooks.
- Set route-specific
parameters.router.initialEntries when a story depends on React Router params.
- Prefer
parameters.layout = "fullscreen" for route, layout, shell, and panel stories that need app-width context.
Verification
- Ensure extracted components do not change behavior.
- Check the related Storybook story was added or updated and exposes controls/actions for every public declared prop.
- Run
npm --workspace @capture-flag/client run storybook:build after Storybook, component, or story changes.
- Run
npm --workspace @capture-flag/client run build after component moves.
Reference: ai/architecture/client-app.md
Client App Architecture
apps/client is a Vite React application for the Capture Flag platform UI.
Entry And Routing
src/main.tsx owns top-level providers.
src/router.tsx owns React Router route definitions.
- Route modules are lazy-loaded through the local
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.
- Selected organization, project, config, and environment state is derived by route helpers such as
useRouteContext, not stored in a mutable layout context.
Route Map
/: 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 /.
Data Flow
- React Query owns server state such as authenticated user, organizations, projects, configs, environments, SDK keys, members, and feature flags.
- API operations live under
src/api/<domain>/<operation>.
- Request functions perform HTTP calls and contain no React imports.
- Query and mutation hooks are the UI-facing API.
- Query hooks may use
useQuery or useInfiniteQuery according to the API shape.
- Mutation hooks invalidate affected query keys.
- Mutations that affect derived server state may invalidate query keys from multiple API domains inside the mutation hook.
- API request and hook tests mock successful responses and API errors instead of reaching a real backend.
- Route params, search params, and server state are combined by
useRouteContext for selected resources and redirect-safe navigation paths.
- Permission gates in the client are UX only; API guards and services remain authoritative.
UI Composition
- Route components compose page sections and own screen-level flow.
- Repeated panels, forms, controls, lists, and empty states move into named components.
- React component files should stay at or below 400 lines by splitting real responsibilities into focused files.
- Short, fixed navigation or action sets should be rendered explicitly instead of through artificial arrays.
- Page-specific components stay colocated under the page folder until reused elsewhere.
- Layout-specific components stay colocated under
src/layouts/<LayoutName> until reused by another layout or page.
- Shared primitives live under
src/components and are imported directly through aliases such as @components/Button.
- Member management uses shared
components/members primitives with page-specific role options.
- Feature flag and segment page internals stay colocated under their page folders until reused.
- Storybook stories live in a
stories/ child folder beside the source area they cover.
- Route-level and cross-page grouping stories live in
src/pages/stories.
- Shared Storybook data and fetch mocks live in
src/stories and are reused by stories and page tests.
Shared Core Utilities
- Context-independent helpers and reusable client hooks live under
src/core/<category>/<name>.ts.
- Current core categories include
date, json, strings, validation, and hooks.
- Each core file exports one function or hook; import it from the direct file path such as
src/core/date/toDate.
- Do not add
index.ts barrels under src/core; multiple helpers require multiple explicit imports.
- Core tests live under
src/core/<category>/__tests__/<name>.test.ts next to the category they cover.
- Client tests live in
__tests__/ child folders beside the source area they cover, mirroring Storybook stories/ folders.
- Shared test setup and helpers live under
src/test and provide Testing Library render helpers, React Query providers, and fetch response mocks.
- Coverage is run with
npm --workspace @capture-flag/client run test:coverage and should stay at 90% or higher for configured client targets.
- Page, domain, API, or route-specific helpers stay colocated with their owning feature until they become context-independent reuse.
Client Test And Story Layout
- API request and hook tests live in
src/api/__tests__ and cover operation behavior through mocked fetch responses.
- Shared component tests live in
src/components/__tests__; member component tests live in src/components/members/__tests__.
- Page tests live in
src/pages/<PageName>/__tests__ when they cover one page folder.
- Page subsection tests live in
src/pages/<PageName>/<section>/__tests__ when they cover colocated page internals such as feature flags or segments.
- Root-level client tests live in
src/__tests__ only for source files owned directly by src, such as permissions.ts.
- Shared component stories live in
src/components/stories; member component stories live in src/components/members/stories.
- Layout stories live in
src/layouts/<LayoutName>/stories.
- Page and page subsection stories live in the owning page folder's
stories/ child folder.
- Cross-page route or panel grouping stories live in
src/pages/stories and use route parameters plus shared mock data to render realistic page states.
- Do not place component stories in
src/stories; that folder is reserved for shared Storybook data, routes, and API mocks.
Form Flow
- React Hook Form owns field state and submission.
- Zod schemas parse and validate form values.
- API mutation hooks submit normalized payloads and refresh server state.
Reference: ai/examples/good-client-component.md
Good Client Component
Source: apps/client/src/components/Button.tsx (sha256: 7e1bcb1c45c5d1a5610f108be5028ce68d34ded2c394d326428eca08702a992d)
Source: apps/client/src/components/Panel.tsx (sha256: c0dfcd9d6984e9741af7abe91c60ad8bcb30e4d8f18c3c1456bdea5882d2472f)
Why this is canonical:
- Accepts native element props instead of inventing a custom prop surface.
- Keeps variants explicit and local to the primitive.
- Uses
classnames as cls for optional classes.
Canonical shared component patterns from apps/client/src/components.
Button Primitive
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.
Panel Wrapper
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.