Skip to content
Back to skills

Shep Ui Component

ASecurity

Use when creating, modifying, or reviewing web UI components. Triggers include "new component", "add component", "create UI", "build a widget", "update component", working with files in src/presentation/web/components/, or when the user asks to build any React component for the web UI. Part of the Shep autonomous SDLC platform — https://shep.bot

  • 262 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 26, 2026
developmenttypescriptgoshellreactnodetestinggitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add shep-ai/shep --skill shep-ui-component --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Shep Ui Component?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Shep Ui Component
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shep-ai-shep-ui-component/badge)](https://www.skillsdirectory.com/skills/shep-ai-shep-ui-component)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: shep:ui-component
description: Use when creating, modifying, or reviewing web UI components. Triggers include "new component", "add component", "create UI", "build a widget", "update component", working with files in src/presentation/web/components/, or when the user asks to build any React component for the web UI. Part of the Shep autonomous SDLC platform — https://shep.bot
metadata:
  version: '1.0.0'
  author: Shep AI (https://shep.bot)
  homepage: https://shep.bot
  repository: https://github.com/shep-ai/shep
---

# Web UI Component Development


Build React components following the four-tier architecture, with mandatory Storybook stories, `data-testid` attributes, and unit tests.

## Four-Tier Hierarchy

```
Tier 0: ui/        -> shadcn/ui primitives (managed by CLI, rarely hand-edited)
Tier 1: common/    -> Reusable composed components (combine ui/ primitives)
Tier 2: layouts/   -> Page shells, structural wrappers (use ui/ + common/)
Tier 3: features/  -> Domain-specific views bound to routes (use all tiers)
```

**Import rule:** A tier may only import from lower tiers, never upward.

```
features/ -> layouts/, common/, ui/
layouts/  -> common/, ui/
common/   -> ui/
ui/       -> external packages only
```

## File Structure

### Tier 0 (ui/) — flat files, no subfolder

```
components/ui/
  button.tsx
  button.stories.tsx
```

### Tier 1-3 — subfolder per component

```
components/common/feature-list-item/
  feature-list-item.tsx          # Implementation
  feature-list-item.stories.tsx  # Storybook stories (MANDATORY)
  index.ts                       # Barrel export
```

**Barrel export template:**

```typescript
export { FeatureListItem } from './feature-list-item';
export type { FeatureListItemProps } from './feature-list-item';
```

After creating any Tier 1-3 component, add it to the tier-level barrel:

- `components/common/index.ts`
- `components/layouts/index.ts`
- `components/features/index.ts`

## Component Template

```typescript
'use client'; // Only if the component uses hooks, event handlers, or browser APIs

import { cn } from '@/lib/utils';

export interface MyComponentProps {
  /** Brief prop description. */
  label: string;
  className?: string;
}

export function MyComponent({ label, className }: MyComponentProps) {
  return (
    <div
      data-testid="my-component"
      className={cn('base-classes', className)}
    >
      {label}
    </div>
  );
}
```

### Rules

1. **Named exports only** — no default exports for components.
2. **`'use client'`** — add only when the component uses hooks, event handlers, or browser APIs. Omit for pure render components.
3. **`className` prop** — accept and merge via `cn()` for composability.
4. **Props interface** — always export the interface alongside the component.

## data-testid Convention

Every component MUST include `data-testid` on its root element for test targeting.

### Naming scheme: `kebab-case`, scoped to the component

| Component               | data-testid               |
| ----------------------- | ------------------------- |
| `FeatureListItem`       | `feature-list-item`       |
| `FeatureStatusGroup`    | `feature-status-group`    |
| `SidebarCollapseToggle` | `sidebar-collapse-toggle` |
| `PageHeader`            | `page-header`             |

### Sub-elements: append a suffix

```typescript
<div data-testid="feature-list-item">
  <span data-testid="feature-list-item-label">{name}</span>
  <span data-testid="feature-list-item-meta">{duration}</span>
</div>
```

### When to add data-testid

- Root element of every component: **always**
- Sub-elements: only when tests need to target them specifically (labels, meta, actions)
- Primitives in `ui/`: use `data-slot` instead (shadcn convention)

### In tests, prefer data-testid queries

```typescript
screen.getByTestId('feature-list-item');
screen.getByTestId('feature-list-item-meta');
```

Fall back to role/text queries when `data-testid` is not set:

```typescript
screen.getByRole('button', { name: /submit/i });
screen.getByText('Auth Module');
```

## Storybook Stories (MANDATORY)

Every component MUST have a colocated `.stories.tsx` file. This is non-negotiable.

### Story template

```typescript
import type { Meta, StoryObj } from '@storybook/react';
import { MyComponent } from './my-component';

// IMPORTANT: Use explicit type annotation, NOT `satisfies Meta<>`
const meta: Meta<typeof MyComponent> = {
  title: 'Composed/MyComponent', // See title prefixes below
  component: MyComponent,
  parameters: {
    layout: 'padded', // 'centered' | 'padded' | 'fullscreen'
  },
  tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
  args: {
    label: 'Example',
  },
};
```

### Title prefixes by tier

| Tier        | Prefix        | Example                    |
| ----------- | ------------- | -------------------------- |
| `ui/`       | `Primitives/` | `Primitives/Button`        |
| `common/`   | `Composed/`   | `Composed/FeatureListItem` |
| `layouts/`  | `Layout/`     | `Layout/AppSidebar`        |
| `features/` | `Features/`   | `Features/VersionPage`     |

### Decorators for context-dependent components

If the component requires a React context (e.g. `SidebarProvider`), wrap it:

```typescript
const meta: Meta<typeof SidebarNavItem> = {
  // ...
  decorators: [
    (Story) => (
      <SidebarProvider>
        <SidebarMenu>
          <Story />
        </SidebarMenu>
      </SidebarProvider>
    ),
  ],
};
```

Story-level decorator overrides (e.g. for alternate states):

```typescript
export const Collapsed: Story = {
  args: { /* ... */ },
  decorators: [
    (Story) => (
      <SidebarProvider defaultOpen={false}>
        <Story />
      </SidebarProvider>
    ),
  ],
};
```

### Args and Controls (CRITICAL)

Storybook controls only appear when stories define `args`. **Never use hardcoded render-only stories** — always define `args` so the Controls panel works.

**Standard components (flat props):** Use `component` in meta and `args` in stories. Controls are auto-generated.

```typescript
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
  args: {
    label: 'Default label',
    variant: 'primary',
  },
};

export const Default: Story = {
  args: {
    label: 'Example',
  },
};
```

**Wrapped/nested-data components (e.g. React Flow nodes):** When a component receives data through a nested object (like `{ data }`) or requires wrapper context that prevents using `component` directly, use the **existing data interface** as the args type. Storybook auto-infers controls from the `args` values — no `argTypes` needed. Do NOT create a duplicate args interface.

```typescript
import type { FeatureNodeData } from './feature-node-state-config';

// 1. Use the component's own data interface — controls auto-inferred from args
const meta: Meta<FeatureNodeData> = {
  title: 'Composed/FeatureNode',
  args: { name: 'Auth Module', state: 'running', progress: 45, featureId: '#f1', lifecycle: 'requirements' },
};

type Story = StoryObj<FeatureNodeData>;

// 2. Pass args directly as data — no mapping function needed
export const Default: Story = {
  render: (args) => <FeatureNode id="n1" data={args} type="featureNode" />,
};

// 3. Stories needing callbacks pass them via story-level args
export const WithAction: Story = {
  args: { onAction: () => undefined, onSettings: () => undefined },
  render: (args) => <FeatureNode id="n1" data={args} type="featureNode" />,
};
```

Only add `argTypes` when you need to **override** defaults (e.g. `select` dropdown instead of free text, `range` slider instead of number input, or `{ table: { disable: true } }` to hide a field).

**Gallery/showcase stories** (AllStates, AllLifecycles) may use hardcoded render without args — controls are not useful when showing all variants at once. But the **Default** story must always have args.

### Story coverage requirements

Stories must cover:

- **Default state** — component with typical props
- **All meaningful variants** — each status, size, or visual variant
- **Edge cases** — empty content, long text, missing optional props
- **Interactive states** — with click handlers (use `() => alert('Clicked!')` or `fn()` from `@storybook/test`)

### Layout parameter guidelines

| Layout       | When to use                                         |
| ------------ | --------------------------------------------------- |
| `centered`   | Small, standalone primitives (Button, Badge, Input) |
| `padded`     | Medium composed components (ListItem, Card, Header) |
| `fullscreen` | Full-width layouts (Sidebar, Dashboard, Page)       |

## Unit Tests

### File location

Mirror the component tier structure under `tests/unit/presentation/web/`:

```
tests/unit/presentation/web/
  button.test.tsx                           # ui/ tier
  common/feature-list-item.test.tsx         # common/ tier
  layouts/app-sidebar.test.tsx              # layouts/ tier
  features/version-page-client.test.tsx     # features/ tier
```

### Test template

```typescript
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MyComponent } from '@/components/common/my-component';

describe('MyComponent', () => {
  it('renders label text', () => {
    render(<MyComponent label="Hello" />);
    expect(screen.getByTestId('my-component')).toBeInTheDocument();
    expect(screen.getByText('Hello')).toBeInTheDocument();
  });

  it('fires onClick when clicked', async () => {
    const handleClick = vi.fn();
    const user = userEvent.setup();
    render(<MyComponent label="Click me" onClick={handleClick} />);
    await user.click(screen.getByTestId('my-component'));
    expect(handleClick).toHaveBeenCalledOnce();
  });

  it('applies custom className', () => {
    render(<MyComponent label="Styled" className="custom-class" />);
    expect(screen.getByTestId('my-component')).toHaveClass('custom-class');
  });
});
```

### Context-dependent component helper

```typescript
import { SidebarProvider } from '@/components/ui/sidebar';

function renderWithSidebar(ui: React.ReactElement) {
  return render(<SidebarProvider>{ui}</SidebarProvider>);
}
```

### Timer/interval testing

```typescript
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

it('updates after 1 second', () => {
  vi.setSystemTime(Date.now());
  render(<ElapsedTime startedAt={Date.now()} />);
  act(() => vi.advanceTimersByTime(1000));
  expect(screen.getByText('00:01')).toBeInTheDocument();
});
```

## Styling Patterns

### Tailwind + cn() for conditional classes

```typescript
import { cn } from '@/lib/utils';

<div className={cn(
  'flex items-center gap-2 rounded-md px-2',
  isActive && 'bg-sidebar-accent text-sidebar-accent-foreground',
  className
)} />
```

### CVA for variant-driven components

```typescript
import { cva, type VariantProps } from 'class-variance-authority';

const myVariants = cva('base-classes', {
  variants: {
    variant: {
      default: 'bg-primary text-primary-foreground',
      outline: 'border bg-background',
    },
    size: {
      default: 'h-9 px-4',
      sm: 'h-7 px-3 text-xs',
    },
  },
  defaultVariants: {
    variant: 'default',
    size: 'default',
  },
});
```

### Design tokens — use semantic color names

```
bg-background, text-foreground           # Page-level
bg-primary, text-primary-foreground      # Brand actions
bg-muted, text-muted-foreground          # De-emphasized
bg-sidebar-accent                        # Sidebar hover/active
text-destructive                         # Errors
border, bg-input                         # Form elements
```

### Numeric display: always use `tabular-nums`

```typescript
<span className="tabular-nums">05:30</span>
```

### Icons: lucide-react

```typescript
import { Home, CircleAlert, Loader2 } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';

// As prop type
interface Props {
  icon: LucideIcon;
}

// Semantic icon coloring
<CircleAlert className="text-amber-500" />
<Loader2 className="text-blue-500 animate-spin" />
<CircleCheck className="text-emerald-500" />
```

## Checklist

Before considering a component done, verify:

- [ ] Component file created with proper structure
- [ ] `data-testid` on root element (and sub-elements where needed)
- [ ] Props interface exported
- [ ] `className` prop accepted and merged via `cn()`
- [ ] Barrel export (`index.ts`) created
- [ ] Tier-level barrel updated (`common/index.ts`, etc.)
- [ ] Storybook stories colocated with all variants covered
- [ ] Default story has `args` defined (controls must work)
- [ ] Stories use explicit `Meta<typeof X>` type annotation (not `satisfies`)
- [ ] Story title uses correct tier prefix (`Primitives/`, `Composed/`, `Layout/`, `Features/`)
- [ ] Unit test created in `tests/unit/presentation/web/[tier]/`
- [ ] `pnpm typecheck:web` passes
- [ ] `pnpm test:single tests/unit/presentation/web` passes
- [ ] `pnpm build:storybook` passes

## Common Mistakes

- **`satisfies Meta<>`** — causes TS2742 error. Use explicit type annotation instead.
- **Missing `'use client'`** — required when using `useState`, `useEffect`, event handlers.
- **Forgetting barrel exports** — both component-level `index.ts` and tier-level barrel.
- **Inline meta in stories** — no default exports, always `export const Default: Story`.
- **Missing `data-testid`** — every component root must have one.
- **Magic numbers in styles** — use design tokens and Tailwind spacing scale.
- **Uppercase in commit subjects** — commitlint rejects it; use all-lowercase.
- **Render-only stories without `args`** — controls panel will be empty. Always define `args`. For nested-data components, reuse the component's data interface as args type (don't create a duplicate), add `argTypes` for control customization, and pass `args` directly as data.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.