Skip to content

Commit f4886cf

Browse files
committed
chore: add building-blocks skill
1 parent d9f5a75 commit f4886cf

20 files changed

Lines changed: 1030 additions & 0 deletions
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Frontend Building Blocks
2+
3+
A structured catalog of typed frontend building blocks, optimized for AI agents.
4+
5+
## Structure
6+
7+
- `rules/` — Individual building block files (one per block)
8+
- `metadata.json` — Catalog metadata (version, categories)
9+
- **`SKILL.md`** — Skill definition with progressive disclosure index
10+
11+
## Building Block Categories
12+
13+
| Category | Blocks | Description |
14+
| ------------------ | ------ | ------------------------------------------------------------------------------ |
15+
| Data Fetching | 4 | Query factories, mutations, keys, DTOs |
16+
| State Management | 2 | Zustand stores, React Context providers |
17+
| App Orchestration | 1 | Use case hooks composing mutations + notifications |
18+
| Component Patterns | 6 | Pure components, compound components, pages, HOCs, facade hooks, named effects |
19+
| Data Modeling | 2 | Frontend models, value objects |
20+
21+
## How It Works
22+
23+
1. Agent reads `SKILL.md` — gets the catalog index with short summaries
24+
2. When implementing a specific block, agent reads `rules/{block-name}.md`
25+
26+
## Creating a New Building Block
27+
28+
1. Create `rules/{block-name}.md`
29+
2. Add frontmatter with `title`, `category`, `layer`, `composedWith`
30+
3. Include: description, constraints, and a canonical code example
31+
4. Update the catalog table in `SKILL.md`
32+
33+
## Block File Structure
34+
35+
```markdown
36+
---
37+
title: Block Display Name
38+
category: Data Fetching | State Management | App Orchestration | Component Patterns | Data Modeling
39+
layer: providers/ | application/ | components/ | models/ | lib/api/ | pages/
40+
composedWith: other-block-1, other-block-2
41+
---
42+
43+
## Block Display Name
44+
45+
Description of what this block does, when to use it, and why.
46+
47+
### Constraints
48+
49+
- Hard rules for this block type
50+
51+
### Example
52+
53+
\`\`\`tsx
54+
// Canonical implementation example
55+
\`\`\`
56+
```
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
name: building-blocks
3+
description: Frontend building block catalog — typed patterns for components, hooks, queries, mutations, stores, and models used across the project. Use this skill whenever implementing, reviewing, or planning feature code, API integrations, state management, component patterns, or data modeling within src/features/ or src/lib/. Also triggers for architectural decisions about where code should live or which pattern to apply.
4+
---
5+
6+
# Frontend Building Blocks
7+
8+
Typed catalog of building blocks used in this project. Each block has a fixed name, layer, composition rules, and canonical example.
9+
10+
## How to Use
11+
12+
Read the rule file for each block you are about to implement. The summaries below are for routing — the rule files contain the full pattern.
13+
14+
## Block Catalog
15+
16+
### Data Fetching
17+
18+
| Block | Layer | Summary | File |
19+
| ----------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
20+
| `mutation-hook` | `providers/` | Server write via `useMutation` with domain error translation and cache invalidation. Returns `[handler, isPending]`. | `rules/mutation-hook.md` |
21+
| `query-options-factory` | `lib/api/` | Reusable `queryOptions()` factories — no hooks, no `useQuery`. Hook composition belongs in `providers/`. | `rules/query-options-factory.md` |
22+
| `query-keys-factory` | `lib/api/` | Hierarchical `as const` key tuples per resource. Single source of truth for cache invalidation. | `rules/query-keys-factory.md` |
23+
| `dto-model` | `lib/api/` | TypeScript interfaces mirroring the raw API wire format. No transformations. | `rules/dto-model.md` |
24+
25+
### State Management
26+
27+
| Block | Layer | Summary | File |
28+
| ---------- | -------------- | -------------------------------------------------------------------------------------------------- | ------------------- |
29+
| `store` | `application/` | Zustand store — small, focused, selector-only subscriptions. Actions live inside the store. | `rules/store.md` |
30+
| `provider` | `providers/` | Thin React Context wrapper for dependency injection. No logic — delivers state, doesn't manage it. | `rules/provider.md` |
31+
32+
### App Orchestration
33+
34+
| Block | Layer | Summary | File |
35+
| --------------- | -------------- | ------------------------------------------------------------------------------------------------ | ------------------------ |
36+
| `use-case-hook` | `application/` | Orchestrates mutation + notification into one feature-level operation. The feature's public API. | `rules/use-case-hook.md` |
37+
38+
### Component Patterns
39+
40+
| Block | Layer | Summary | File |
41+
| -------------------- | -------------- | -------------------------------------------------------------------------------------------------- | ----------------------------- |
42+
| `notification-hook` | `application/` | Maps operation outcomes to toast notifications. One hook per use case. | `rules/notification-hook.md` |
43+
| `pure-component` | `components/` | Props in, JSX out. No side effects, no internal state beyond `useMemo`. | `rules/pure-component.md` |
44+
| `compound-component` | `components/` | Dot-notation sub-components. Composition over configuration — no boolean prop toggles. | `rules/compound-component.md` |
45+
| `hoc` | `components/` | Component in → enhanced component out. For render-level decisions (auth gates, suspense wrappers). | `rules/hoc.md` |
46+
| `page` | `pages/` | Route-level orchestrator. Only place where router coupling is acceptable. | `rules/page.md` |
47+
| `facade-hook` | `components/` | Private logic extraction for a single component. Defined below the component, not exported. | `rules/facade-hook.md` |
48+
| `named-effect` | `components/` | Named function expressions in `useEffect`. Intent visible at a glance. | `rules/named-effect.md` |
49+
50+
### Data Modeling
51+
52+
| Block | Layer | Summary | File |
53+
| ---------------- | -------------- | ------------------------------------------------------------------------------------------ | ------------------------- |
54+
| `frontend-model` | `models/` | UI-friendly domain objects mapped from DTOs. Skip if DTO shape is identical. | `rules/frontend-model.md` |
55+
| `value-object` | `application/` | Static-method classes grouping domain logic for a single concept. Namespace, not instance. | `rules/value-object.md` |
56+
57+
## Rules
58+
59+
- Block files live in `rules/` — one file per block.
60+
- Each file contains: description, constraints, and a canonical code example.
61+
- Block names are stable identifiers referenced in specs and task breakdowns.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"version": "1.0.0",
3+
"date": "April 2026",
4+
"abstract": "Typed catalog of frontend building blocks. Contains 15 blocks across 5 categories (Data Fetching, State Management, App Orchestration, Component Patterns, Data Modeling). Each block defines composition rules, layer constraints, and canonical implementation examples. Designed for AI agents — read the index to route, read the rule file to implement.",
5+
"categories": [
6+
{
7+
"name": "Data Fetching",
8+
"blocks": [
9+
"mutation-hook",
10+
"query-options-factory",
11+
"query-keys-factory",
12+
"dto-model"
13+
]
14+
},
15+
{
16+
"name": "State Management",
17+
"blocks": ["store", "provider"]
18+
},
19+
{
20+
"name": "App Orchestration",
21+
"blocks": ["use-case-hook"]
22+
},
23+
{
24+
"name": "Component Patterns",
25+
"blocks": [
26+
"notification-hook",
27+
"pure-component",
28+
"compound-component",
29+
"hoc",
30+
"page",
31+
"facade-hook",
32+
"named-effect"
33+
]
34+
},
35+
{
36+
"name": "Data Modeling",
37+
"blocks": ["frontend-model", "value-object"]
38+
}
39+
]
40+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
title: Compound Component
3+
category: Component Patterns
4+
layer: components/
5+
composedWith: pure-component, facade-hook
6+
---
7+
8+
## Compound Component
9+
10+
Parent exposes dot-notation sub-components, consumers compose only the parts they need. Single import, internal structure hidden. The go-to for complex UI widgets (Tabs, Accordion, Select, Menu) where parts must coordinate but consumers need control over layout.
11+
12+
### Constraints
13+
14+
- Composition over configuration. If you're adding boolean props like `showHeader`, `showFooter`, `withSearch` — stop. Let the consumer compose the parts they need instead. Structure should be visible in JSX, not buried in prop logic.
15+
- Context is optional. Sometimes the pattern is just about providing a clean dot-notation API and avoiding falsy props that a root component would only pass through to its internal children. Use Context when sub-components genuinely need to coordinate shared state; skip it when the sub-components are independent.
16+
- Warning signs you need this: multiple boolean props toggling sections, large config objects controlling internal rendering, difficulty adding variations without modifying the component.
17+
- Don't over-apply. Single-purpose components with no optional sections stay as single units. Compound is for when consumers need structural flexibility.
18+
19+
### Example
20+
21+
```tsx
22+
// ❌ Prop-based configuration — inflexible, combinatorial explosion
23+
interface Props {
24+
showHeader: boolean;
25+
showSearch: boolean;
26+
actions: Action[];
27+
}
28+
29+
function ActionsMenu({ showHeader, showSearch, actions }: Props) {
30+
return (
31+
<div>
32+
{showHeader && <h2>Actions</h2>}
33+
{showSearch && <SearchInput />}
34+
<ul>
35+
{actions.map((action) => (
36+
<li key={action.id}>{action.label}</li>
37+
))}
38+
</ul>
39+
</div>
40+
);
41+
}
42+
```
43+
44+
```tsx
45+
// ✅ Composition-based — each sub-component has one job,
46+
// structure is visible in JSX, only render what you need.
47+
// No Context needed here — sub-components are independent.
48+
function ActionsMenu({ children }: { children: React.ReactNode }) {
49+
return <div className="actions-menu">{children}</div>;
50+
}
51+
52+
ActionsMenu.Header = ({ children }: { children: React.ReactNode }) => (
53+
<div className="actions-menu__header">{children}</div>
54+
);
55+
56+
ActionsMenu.Search = ({ onSearch }: { onSearch: (query: string) => void }) => (
57+
<input onChange={(e) => onSearch(e.target.value)} />
58+
);
59+
60+
ActionsMenu.List = ({ children }: { children: React.ReactNode }) => (
61+
<ul className="actions-menu__list">{children}</ul>
62+
);
63+
64+
ActionsMenu.Item = ({
65+
children,
66+
onClick,
67+
}: {
68+
children: React.ReactNode;
69+
onClick: () => void;
70+
}) => <li onClick={onClick}>{children}</li>;
71+
```
72+
73+
```tsx
74+
// Consumer composes only what they need
75+
<ActionsMenu>
76+
<ActionsMenu.Header>Actions</ActionsMenu.Header>
77+
<ActionsMenu.Search onSearch={handleSearch} />
78+
<ActionsMenu.List>
79+
{actions.map((action) => (
80+
<ActionsMenu.Item key={action.id} onClick={action.handler}>
81+
{action.label}
82+
</ActionsMenu.Item>
83+
))}
84+
</ActionsMenu.List>
85+
</ActionsMenu>
86+
```
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
title: Dto Model
3+
category: Data Fetching
4+
layer: lib/api/
5+
composedWith: query-options-factory
6+
---
7+
8+
## Dto Model
9+
10+
TypeScript interfaces representing raw API response shapes — the wire format. Never use directly in UI or application logic; map to domain models when the shape diverges. Co-located with the `query-options-factory` that fetches the data.
11+
12+
### Constraints
13+
14+
- DTOs mirror the API contract exactly — no transformations, no computed fields, no UI conveniences. If the API returns `snake_case`, the DTO uses `snake_case`.
15+
- One DTO file per resource endpoint. Shared sub-types (like `Rating`) live in the same file if they're only used by that DTO.
16+
- Enums in DTOs represent server-defined value sets. Use `enum` when the API guarantees a closed set, union types when it doesn't.
17+
18+
### Example
19+
20+
```tsx
21+
export enum Category {
22+
Men_clothing = "men's clothing",
23+
Women_clothing = "women's clothing",
24+
Jewelery = "jewelery",
25+
Electronics = "electronics",
26+
}
27+
28+
export interface Rating {
29+
rate: number;
30+
count: number;
31+
}
32+
33+
export interface ProductDto {
34+
id: number;
35+
title: string;
36+
description: string;
37+
category: Category;
38+
image: string;
39+
price: number;
40+
rating: Rating;
41+
}
42+
```
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
title: Facade Hook
3+
category: Component Patterns
4+
layer: components/
5+
composedWith: pure-component, compound-component
6+
---
7+
8+
## Facade Hook
9+
10+
Encapsulates logic a component needs so the component body stays pure — focused entirely on rendering JSX. Defined below the component in the same file. Think of it as the component's private method: not exported, not reused, exists solely to keep the render function clean.
11+
12+
### Constraints
13+
14+
- One facade hook per component. If you need two, the component probably has two responsibilities.
15+
- The hook returns exactly what the component's JSX needs — pre-shaped data, handlers, derived values. The component never transforms what the hook returns.
16+
- Facade hooks are for logic extraction, not data fetching orchestration. Loading states, queries, and mutations stay in the component or page. The facade hook handles formatting, derivations, and non-trivial transformations.
17+
- Don't confuse with shared hooks. A facade hook is private to its component. If multiple components need the same logic, that's a shared hook or `use-case-hook`, not a facade.
18+
19+
### Example
20+
21+
```tsx
22+
interface Props {
23+
product: Product;
24+
locale: string;
25+
}
26+
27+
// Component stays focused on rendering
28+
export const ProductHeader = ({ product }: Props) => {
29+
const { displayTitle, priceLabel, badge } = useFormattedProduct(product);
30+
31+
return (
32+
<div className="product-header">
33+
<h1>{displayTitle}</h1>
34+
{badge && <span className="badge">{badge}</span>}
35+
<span className="price">{priceLabel}</span>
36+
</div>
37+
);
38+
};
39+
40+
// Facade hook — private, not exported, defined below the component
41+
const useFormattedProduct = (product: Product) => {
42+
const displayTitle = useMemo(() => {
43+
const base = product.name.trim();
44+
const variant = product.variant ? ` — ${product.variant}` : "";
45+
const limited = product.isLimitedEdition ? " (Limited Edition)" : "";
46+
return `${base}${variant}${limited}`;
47+
}, [product.name, product.variant, product.isLimitedEdition]);
48+
49+
const priceLabel = useMemo(() => {
50+
const formatter = new MoneyVO.format({
51+
style: "currency",
52+
currency: product.currency,
53+
});
54+
return product.discountPrice
55+
? `${formatter.format(product.discountPrice)} (was ${formatter.format(product.price)})`
56+
: formatter.format(product.price);
57+
}, [product.price, product.discountPrice, product.currency]);
58+
59+
const badge = useMemo(() => {
60+
if (product.stock === 0) return "Out of Stock";
61+
if (product.stock <= 3) return `Only ${product.stock} left`;
62+
if (product.isNew) return "New";
63+
return null;
64+
}, [product.stock, product.isNew]);
65+
66+
return { displayTitle, priceLabel, badge };
67+
};
68+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
title: Frontend Model
3+
category: Data Modeling
4+
layer: models/
5+
composedWith: dto-model
6+
---
7+
8+
## Frontend Model
9+
10+
Frontend-friendly domain objects parsed or transformed from `dto-model`. Contains the shape your UI actually needs — renamed fields, computed properties, flattened nesting. Defined in `models/` of the feature slice.
11+
12+
### Constraints
13+
14+
- DTOs are the wire format, frontend models are the app format. When they diverge, map explicitly — don't let DTO shapes leak into components or application.
15+
- Keep models as plain interfaces/types — no methods, no class instances. Domain logic lives in `value-object` if needed.
16+
- If the DTO and frontend model are identical, skip the frontend model. Don't add a layer just for the sake of it.
17+
18+
### Example
19+
20+
```tsx
21+
// models/product.ts — what the UI actually works with
22+
export interface Product {
23+
id: string;
24+
name: string; // renamed from dto.title
25+
price: number;
26+
category: string;
27+
imageUrl: string; // renamed from dto.image
28+
rating: number; // flattened from dto.rating.rate
29+
reviewCount: number; // flattened from dto.rating.count
30+
}
31+
```

0 commit comments

Comments
 (0)