pasika 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -55
- package/dist/eslint/pasika/index.d.ts +18 -0
- package/dist/eslint/pasika/index.js +21 -3
- package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-barrel-exports.js +78 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +84 -0
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +76 -0
- package/dist/eslint/pasika/rules/filename-case.d.ts +2 -0
- package/dist/eslint/pasika/rules/filename-case.js +100 -0
- package/dist/eslint/pasika/rules/import-boundaries.d.ts +2 -0
- package/dist/eslint/pasika/rules/{organization-imports.js → import-boundaries.js} +11 -1
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +9 -0
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +107 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +9 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +84 -0
- package/package.json +5 -16
- package/claude/hooks/.vulyk +0 -3
- package/claude/hooks/AGENTS.md +0 -3
- package/claude/hooks/CLAUDE.md +0 -1
- package/claude/hooks/claude-hooks.md +0 -30
- package/claude/hooks/notification.sh +0 -38
- package/claude/hooks/protect-files.sh +0 -21
- package/claude/hooks/status-line/index.js +0 -57
- package/claude/scripts/render-settings.ts +0 -223
- package/claude/settings.base.json +0 -38
- package/dist/claude/scripts/render-settings.js +0 -145
- package/dist/eslint/pasika/rules/organization-imports.d.ts +0 -2
- package/dist/eslint.config.js +0 -7
- package/dist/scripts/pasika.js +0 -59
- package/docs/agent-conventions.md +0 -21
- package/docs/claude/hooks.md +0 -30
- package/docs/code-organization-guide/code-organization-guide.md +0 -65
- package/docs/code-organization-guide/references/application-architecture-reference.md +0 -107
- package/docs/code-organization-guide/rules/component-placement-rule.md +0 -135
- package/docs/code-organization-guide/rules/configuration-rule.md +0 -42
- package/docs/code-organization-guide/rules/constants-rule.md +0 -77
- package/docs/code-organization-guide/rules/exports-and-imports-rule.md +0 -81
- package/docs/code-organization-guide/rules/folder-nesting-rule.md +0 -83
- package/docs/code-organization-guide/rules/hook-extraction-rule.md +0 -142
- package/docs/code-organization-guide/rules/interactive-component-rule.md +0 -100
- package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +0 -68
- package/docs/code-organization-guide/rules/locales-rule.md +0 -55
- package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +0 -66
- package/docs/code-organization-guide/rules/native-prop-forwarding-rule.md +0 -38
- package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +0 -64
- package/docs/code-organization-guide/rules/repeated-structure-rule.md +0 -93
- package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +0 -116
- package/docs/code-organization-guide/rules/sole-state-owner-rule.md +0 -102
- package/docs/code-organization-guide/rules/types-and-schemas-rule.md +0 -138
- package/docs/code-organization-guide/rules/utilities-rule.md +0 -87
- package/docs/documentation-guide/_templates/guide.md +0 -19
- package/docs/documentation-guide/_templates/reference.md +0 -17
- package/docs/documentation-guide/_templates/rule.md +0 -21
- package/docs/documentation-guide/documentation-guide.md +0 -13
- package/docs/documentation-guide/references/documentation-types-reference.md +0 -9
- package/docs/documentation-guide/rules/guide-creation-rule.md +0 -112
- package/docs/documentation-guide/rules/reference-creation-rule.md +0 -132
- package/docs/documentation-guide/rules/rule-creation-rule.md +0 -81
- package/docs/documentation-guide/rules/template-usage-rule.md +0 -48
- package/docs/shadcn-theme.md +0 -121
- package/docs/styling-guide/rules/class-composition-rule.md +0 -32
- package/docs/styling-guide/rules/color-role-naming-rule.md +0 -115
- package/docs/styling-guide/rules/component-state-rule.md +0 -26
- package/docs/styling-guide/rules/component-variant-rule.md +0 -128
- package/docs/styling-guide/rules/global-style-system-rule.md +0 -63
- package/docs/styling-guide/rules/style-placement-rule.md +0 -49
- package/docs/styling-guide/rules/tailwind-utility-rule.md +0 -48
- package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +0 -129
- package/docs/styling-guide/rules/theme-token-rule.md +0 -32
- package/docs/styling-guide/styling-guide.md +0 -17
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Native Prop Forwarding Rule
|
|
2
|
-
|
|
3
|
-
When a `src/shared/` component renders the same intrinsic root element in every rendered result, a smaller custom prop interface can hide capabilities that consumers need. This rule preserves native props while allowing focused component-specific props.
|
|
4
|
-
|
|
5
|
-
- A `src/shared/` component with the same intrinsic root element in every rendered result MUST include that element's native props in its props type.
|
|
6
|
-
- A `src/shared/` component with the same intrinsic root element in every rendered result MUST forward unconsumed native props to that element.
|
|
7
|
-
- A `src/shared/` component with the same intrinsic root element in every rendered result MAY add component-specific props alongside its native props.
|
|
8
|
-
|
|
9
|
-
## Incorrect — Native Props Replaced
|
|
10
|
-
|
|
11
|
-
```tsx
|
|
12
|
-
// src/shared/button.tsx
|
|
13
|
-
type ButtonProps = {
|
|
14
|
-
label: string;
|
|
15
|
-
onClick(): void;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function Button({ label, onClick }: ButtonProps) {
|
|
19
|
-
return <button onClick={onClick}>{label}</button>;
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Why: the custom interface leaves out native button props such as `disabled`, `type`, and accessible labels.
|
|
24
|
-
|
|
25
|
-
## Correct — Native Props Extended and Forwarded
|
|
26
|
-
|
|
27
|
-
```tsx
|
|
28
|
-
// src/shared/button.tsx
|
|
29
|
-
type ButtonProps = React.ComponentProps<"button"> & {
|
|
30
|
-
loading?: boolean;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export function Button({ className, loading, disabled, ...props }: ButtonProps) {
|
|
34
|
-
return <button className={cn("inline-flex items-center", className)} disabled={disabled || loading} {...props} />;
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Why: the component forwards its unused native props while adding the `loading` state it needs.
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# No Mixed Concerns Rule
|
|
2
|
-
|
|
3
|
-
One component per file keeps components easy to find and change independently. This rule applies the same requirement to copied and generated source.
|
|
4
|
-
|
|
5
|
-
- A `.tsx` file that defines a component MUST contain exactly one component.
|
|
6
|
-
- A non-component module MUST use `.ts`, except when a framework requires a `.tsx` routing file.
|
|
7
|
-
|
|
8
|
-
## Incorrect — Two Components in One File
|
|
9
|
-
|
|
10
|
-
Two components in one file:
|
|
11
|
-
|
|
12
|
-
```tsx
|
|
13
|
-
// src/features/nav/menu.tsx
|
|
14
|
-
export function Menu(): React.JSX.Element {
|
|
15
|
-
return (
|
|
16
|
-
<nav>
|
|
17
|
-
<MenuItem label="Home" />
|
|
18
|
-
<MenuItem label="About" />
|
|
19
|
-
</nav>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function MenuItem({ label }: { label: string }): React.JSX.Element {
|
|
24
|
-
return <a href="#">{label}</a>;
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Why: two components share `menu.tsx`, so a reviewer searching for `MenuItem` cannot find it in the file tree.
|
|
29
|
-
|
|
30
|
-
## Correct — Extracted Child in Its Own File
|
|
31
|
-
|
|
32
|
-
One component per file, with the extracted child nested under its owner:
|
|
33
|
-
|
|
34
|
-
```text
|
|
35
|
-
src/features/nav/
|
|
36
|
-
menu/
|
|
37
|
-
index.ts # re-exports only menu.tsx
|
|
38
|
-
menu.tsx
|
|
39
|
-
menu-item.tsx # exclusive child — imported directly by menu.tsx
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
```tsx
|
|
43
|
-
// src/features/nav/menu/menu-item.tsx
|
|
44
|
-
export function MenuItem({ label }: { label: string }): React.JSX.Element {
|
|
45
|
-
return <a href="#">{label}</a>;
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
```tsx
|
|
50
|
-
// src/features/nav/menu/menu.tsx
|
|
51
|
-
import { MenuItem } from "./menu-item";
|
|
52
|
-
import { locales } from "@/locales";
|
|
53
|
-
|
|
54
|
-
export function Menu(): React.JSX.Element {
|
|
55
|
-
return (
|
|
56
|
-
<nav>
|
|
57
|
-
<MenuItem label={locales.nav.home} />
|
|
58
|
-
<MenuItem label={locales.nav.about} />
|
|
59
|
-
</nav>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Why: `Menu` and `MenuItem` each have their own file, so both are independently searchable.
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Repeated Structure Rule
|
|
2
|
-
|
|
3
|
-
Repeated markup can drift when one copy changes and another does not. This rule makes repeated structure a clear extraction trigger.
|
|
4
|
-
|
|
5
|
-
- A block of elements MUST be extracted as a named component when the same structural frame and semantic purpose appear in two or more places. Different data or labels do not prevent extraction.
|
|
6
|
-
|
|
7
|
-
## Incorrect — Repeated Structure Kept Inline
|
|
8
|
-
|
|
9
|
-
```tsx
|
|
10
|
-
// src/features/dashboard/dashboard-view.tsx
|
|
11
|
-
import { locales } from "@/locales";
|
|
12
|
-
|
|
13
|
-
export function DashboardView({ stats, activity }: DashboardViewProps): React.JSX.Element {
|
|
14
|
-
return (
|
|
15
|
-
<main>
|
|
16
|
-
<section className="w-full rounded border p-6">
|
|
17
|
-
<h2>{locales.dashboard.stats}</h2>
|
|
18
|
-
<ul>
|
|
19
|
-
{stats.map((stat) => (
|
|
20
|
-
<li key={stat.id}>
|
|
21
|
-
{stat.label}: {stat.value}
|
|
22
|
-
</li>
|
|
23
|
-
))}
|
|
24
|
-
</ul>
|
|
25
|
-
</section>
|
|
26
|
-
|
|
27
|
-
<section className="w-full rounded border p-6">
|
|
28
|
-
<h2>{locales.dashboard.recentActivity}</h2>
|
|
29
|
-
<ul>
|
|
30
|
-
{activity.map((event) => (
|
|
31
|
-
<li key={event.id}>
|
|
32
|
-
{event.label}: {event.value}
|
|
33
|
-
</li>
|
|
34
|
-
))}
|
|
35
|
-
</ul>
|
|
36
|
-
</section>
|
|
37
|
-
</main>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Why: the same section, heading, and list structure appears in two places. Changing the shared frame requires editing both copies.
|
|
43
|
-
|
|
44
|
-
## Correct — Repeated Structure Extracted
|
|
45
|
-
|
|
46
|
-
```text
|
|
47
|
-
src/features/dashboard/
|
|
48
|
-
dashboard-view/
|
|
49
|
-
index.ts # re-exports only dashboard-view.tsx
|
|
50
|
-
dashboard-view.tsx
|
|
51
|
-
panel.tsx # exclusive child — imported directly
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
```tsx
|
|
55
|
-
// src/features/dashboard/dashboard-view/panel.tsx
|
|
56
|
-
type PanelItem = {
|
|
57
|
-
id: string;
|
|
58
|
-
label: string;
|
|
59
|
-
value: string | number;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export function Panel({ title, items }: { title: string; items: PanelItem[] }): React.JSX.Element {
|
|
63
|
-
return (
|
|
64
|
-
<section className="w-full rounded border p-6">
|
|
65
|
-
<h2>{title}</h2>
|
|
66
|
-
<ul>
|
|
67
|
-
{items.map((item) => (
|
|
68
|
-
<li key={item.id}>
|
|
69
|
-
{item.label}: {item.value}
|
|
70
|
-
</li>
|
|
71
|
-
))}
|
|
72
|
-
</ul>
|
|
73
|
-
</section>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
```tsx
|
|
79
|
-
// src/features/dashboard/dashboard-view/dashboard-view.tsx
|
|
80
|
-
import { Panel } from "./panel";
|
|
81
|
-
import { locales } from "@/locales";
|
|
82
|
-
|
|
83
|
-
export function DashboardView({ stats, activity }: DashboardViewProps): React.JSX.Element {
|
|
84
|
-
return (
|
|
85
|
-
<main>
|
|
86
|
-
<Panel title={locales.dashboard.stats} items={stats} />
|
|
87
|
-
<Panel title={locales.dashboard.recentActivity} items={activity} />
|
|
88
|
-
</main>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Why: `Panel` owns the shared section, heading, and list structure, while each call site supplies only its title and items.
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# Smart vs Dumb Component Rule
|
|
2
|
-
|
|
3
|
-
Without a file-name convention, a component's smart vs dumb ownership is invisible to reviewers from the tree alone. Without a `data-testid` matching the file's casing, tests hardcode DOM identities that break on rename or restructure.
|
|
4
|
-
|
|
5
|
-
- Remote or shared application data or state is data from a server, context, store, URL, or server action. A component MUST be smart when it fetches or mutates that data, performs an effect, or defines a callback that changes it, including through a hook or service.
|
|
6
|
-
- A component is considered to perform these operations when it calls a custom hook that performs them.
|
|
7
|
-
- In this rule, an effect MUST be a subscription, external I/O, DOM manipulation, or resource lifecycle.
|
|
8
|
-
- Local UI state and callbacks that only update it MUST NOT make a component smart, including when passed to another component or directly to a native element.
|
|
9
|
-
- A dumb component MUST NOT do any of these.
|
|
10
|
-
- A component that does none of these MUST be dumb.
|
|
11
|
-
- A smart component file name MUST be `PascalCase.tsx`.
|
|
12
|
-
- A dumb component file name MUST be `kebab-case.tsx`.
|
|
13
|
-
- A smart component with one outer DOM element in every rendered result MUST set `data-testid` on that element, and its value MUST match the component name in `PascalCase`.
|
|
14
|
-
- A smart component without one outer DOM element in every rendered result MAY omit `data-testid`.
|
|
15
|
-
- A dumb component MAY set `data-testid` on its root element, and the value MUST be `kebab-case`.
|
|
16
|
-
- [Next.js App Router routing files](https://nextjs.org/docs/app/getting-started/project-structure#routing-files) MUST use their required kebab-case names and are exempt from smart/dumb file-name and `data-testid` requirements.
|
|
17
|
-
|
|
18
|
-
## Incorrect — Smart Component Uses a Dumb Name
|
|
19
|
-
|
|
20
|
-
```tsx
|
|
21
|
-
// src/features/social/social-stats-panel.tsx
|
|
22
|
-
"use client";
|
|
23
|
-
|
|
24
|
-
import { useSocialStats } from "./hooks/use-social-stats";
|
|
25
|
-
import { PlatformCard } from "./platform-card";
|
|
26
|
-
import { locales } from "@/locales";
|
|
27
|
-
|
|
28
|
-
export function SocialStatsPanel(): React.JSX.Element {
|
|
29
|
-
const { stats, isLoading } = useSocialStats();
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div data-testid="social-stats-panel">
|
|
33
|
-
{isLoading ? (
|
|
34
|
-
<p>{locales.social.loading}</p>
|
|
35
|
-
) : (
|
|
36
|
-
stats.map((stat) => <PlatformCard key={stat.platform} data={stat} />)
|
|
37
|
-
)}
|
|
38
|
-
</div>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Why: fetching data makes the component smart, but its file name and `data-testid` use dumb-component casing.
|
|
44
|
-
|
|
45
|
-
## Correct — Smart Component Uses a Smart Name
|
|
46
|
-
|
|
47
|
-
```tsx
|
|
48
|
-
// src/features/social/SocialStatsPanel.tsx
|
|
49
|
-
"use client";
|
|
50
|
-
|
|
51
|
-
import { useSocialStats } from "./hooks/use-social-stats";
|
|
52
|
-
import { PlatformCard } from "./platform-card";
|
|
53
|
-
import { locales } from "@/locales";
|
|
54
|
-
|
|
55
|
-
export function SocialStatsPanel(): React.JSX.Element {
|
|
56
|
-
const { stats, isLoading } = useSocialStats();
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div data-testid="SocialStatsPanel">
|
|
60
|
-
{isLoading ? (
|
|
61
|
-
<p>{locales.social.loading}</p>
|
|
62
|
-
) : (
|
|
63
|
-
stats.map((stat) => <PlatformCard key={stat.platform} data={stat} />)
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Why: fetching data makes the component smart, so its file name and `data-testid` use `PascalCase`.
|
|
71
|
-
|
|
72
|
-
## Incorrect — Dumb Component Uses a Smart Name
|
|
73
|
-
|
|
74
|
-
```tsx
|
|
75
|
-
// src/features/social/PlatformCard.tsx
|
|
76
|
-
|
|
77
|
-
export function PlatformCard({
|
|
78
|
-
data,
|
|
79
|
-
onFollowClick,
|
|
80
|
-
}: {
|
|
81
|
-
data: PlatformStat;
|
|
82
|
-
onFollowClick: () => void;
|
|
83
|
-
}): React.JSX.Element {
|
|
84
|
-
return (
|
|
85
|
-
<article data-testid="PlatformCard">
|
|
86
|
-
<h3>{data.platform}</h3>
|
|
87
|
-
<FollowButton onFollowClick={onFollowClick} />
|
|
88
|
-
</article>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Why: the component fetches no data and only receives a child callback, so it is dumb. Its file name and `data-testid` use smart-component casing.
|
|
94
|
-
|
|
95
|
-
## Correct — Dumb Component Uses a Dumb Name
|
|
96
|
-
|
|
97
|
-
```tsx
|
|
98
|
-
// src/features/social/platform-card.tsx
|
|
99
|
-
|
|
100
|
-
export function PlatformCard({
|
|
101
|
-
data,
|
|
102
|
-
onFollowClick,
|
|
103
|
-
}: {
|
|
104
|
-
data: PlatformStat;
|
|
105
|
-
onFollowClick: () => void;
|
|
106
|
-
}): React.JSX.Element {
|
|
107
|
-
return (
|
|
108
|
-
<article data-testid="platform-card">
|
|
109
|
-
<h3>{data.platform}</h3>
|
|
110
|
-
<FollowButton onFollowClick={onFollowClick} />
|
|
111
|
-
</article>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Why: the component is dumb, so its file name and `data-testid` use `kebab-case`.
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# Sole State Owner Rule
|
|
2
|
-
|
|
3
|
-
Some blocks of elements are the only consumers of a state hook. This rule extracts those blocks into components that own the hook.
|
|
4
|
-
|
|
5
|
-
- A state hook's consumers are the JSX, callbacks, and effects that read its state value or call its updater.
|
|
6
|
-
- A proper sub-block of a component's output MUST be extracted when it contains every consumer of one state hook and one clear component name describes it. A consumer declared before `return` belongs to a sub-block only when it is used exclusively by that sub-block; otherwise this rule does not require extraction. The component that owns the state is not itself an extraction trigger.
|
|
7
|
-
|
|
8
|
-
## Incorrect — Parent Keeps Child-Only State
|
|
9
|
-
|
|
10
|
-
```tsx
|
|
11
|
-
// src/compositions/dashboard-view.tsx
|
|
12
|
-
import { locales } from "@/locales";
|
|
13
|
-
|
|
14
|
-
export function DashboardView(): React.JSX.Element {
|
|
15
|
-
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div>
|
|
19
|
-
<h1>{locales.dashboard}</h1>
|
|
20
|
-
<button onClick={() => setIsHelpOpen(true)} type="button">
|
|
21
|
-
{locales.help}
|
|
22
|
-
</button>
|
|
23
|
-
{isHelpOpen && (
|
|
24
|
-
<Modal onClose={() => setIsHelpOpen(false)}>
|
|
25
|
-
<HelpContent />
|
|
26
|
-
</Modal>
|
|
27
|
-
)}
|
|
28
|
-
<DashboardContent />
|
|
29
|
-
</div>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Why: the help button and modal are the only users of `isHelpOpen`, but the state lives in `DashboardView`.
|
|
35
|
-
|
|
36
|
-
## Correct — Child Owns Its State
|
|
37
|
-
|
|
38
|
-
```text
|
|
39
|
-
src/compositions/
|
|
40
|
-
dashboard-view/
|
|
41
|
-
index.ts # re-exports only dashboard-view.tsx
|
|
42
|
-
dashboard-view.tsx
|
|
43
|
-
help-dialog/
|
|
44
|
-
index.ts # re-exports only help-dialog.tsx
|
|
45
|
-
help-dialog.tsx
|
|
46
|
-
help-trigger.tsx # exclusive child — imported directly
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
```tsx
|
|
50
|
-
// src/compositions/dashboard-view/help-dialog/help-dialog.tsx
|
|
51
|
-
import { HelpTrigger } from "./help-trigger";
|
|
52
|
-
|
|
53
|
-
export function HelpDialog(): React.JSX.Element {
|
|
54
|
-
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<>
|
|
58
|
-
<HelpTrigger onOpen={() => setIsHelpOpen(true)} />
|
|
59
|
-
{isHelpOpen && (
|
|
60
|
-
<Modal onClose={() => setIsHelpOpen(false)}>
|
|
61
|
-
<HelpContent />
|
|
62
|
-
</Modal>
|
|
63
|
-
)}
|
|
64
|
-
</>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
```tsx
|
|
70
|
-
// src/compositions/dashboard-view/help-dialog/help-trigger.tsx
|
|
71
|
-
import { locales } from "@/locales";
|
|
72
|
-
|
|
73
|
-
type HelpTriggerProps = {
|
|
74
|
-
onOpen: () => void;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export function HelpTrigger({ onOpen }: HelpTriggerProps): React.JSX.Element {
|
|
78
|
-
return (
|
|
79
|
-
<button onClick={onOpen} type="button">
|
|
80
|
-
{locales.help}
|
|
81
|
-
</button>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
```tsx
|
|
87
|
-
// src/compositions/dashboard-view/dashboard-view.tsx
|
|
88
|
-
import { HelpDialog } from "./help-dialog";
|
|
89
|
-
import { locales } from "@/locales";
|
|
90
|
-
|
|
91
|
-
export function DashboardView(): React.JSX.Element {
|
|
92
|
-
return (
|
|
93
|
-
<div>
|
|
94
|
-
<h1>{locales.dashboard}</h1>
|
|
95
|
-
<HelpDialog />
|
|
96
|
-
<DashboardContent />
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Why: `HelpDialog` owns the help button, modal, and `isHelpOpen` state. `DashboardView` only composes it.
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
# Types and Schemas Rule
|
|
2
|
-
|
|
3
|
-
Types and schemas are easy to bury in component files or scatter across the project. This rule keeps them close to one component and gives independently used ones a consistent location.
|
|
4
|
-
|
|
5
|
-
- A type or schema used only to implement one configuration object MUST live in that object's `types/` or `schemas/` folder.
|
|
6
|
-
- Except for a type or schema used only to implement one configuration object, a type or schema declared in a component MUST stay in that component file until another file imports it without the component where it is defined. Importing it alongside that component does not trigger extraction.
|
|
7
|
-
- Except for a type or schema used only to implement one configuration object, a type or schema declared outside a component MUST stay with its declaring module until another file imports it independently.
|
|
8
|
-
- Extracted types and schemas MUST live in their matching `types/` or `schemas/` folder at the closest common folder (CCF) of their consumers.
|
|
9
|
-
- A `types/` or `schemas/` `index.ts` MAY define its exports directly.
|
|
10
|
-
- Types and schemas that are used together MAY be grouped in a file with a kebab-case name and named-re-exported from `index.ts`.
|
|
11
|
-
- When a `types/` or `schemas/` folder uses grouped files, its `index.ts` MUST only named-re-export those files.
|
|
12
|
-
- When a type or schema's CCF is `src/features/`, it MUST move to `src/types/` or `src/schemas/`.
|
|
13
|
-
|
|
14
|
-
## Incorrect — Feature and Composition Type Kept in a Feature
|
|
15
|
-
|
|
16
|
-
```text
|
|
17
|
-
src/
|
|
18
|
-
├── compositions/
|
|
19
|
-
│ └── BillingDashboard.tsx
|
|
20
|
-
├── features/
|
|
21
|
-
│ └── billing/
|
|
22
|
-
│ ├── invoice.tsx
|
|
23
|
-
│ └── types/
|
|
24
|
-
│ ├── billing.ts
|
|
25
|
-
│ └── index.ts
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
```tsx
|
|
29
|
-
// src/features/billing/invoice.tsx
|
|
30
|
-
import type { InvoiceStatus } from "./types";
|
|
31
|
-
|
|
32
|
-
// src/compositions/BillingDashboard.tsx
|
|
33
|
-
import type { InvoiceStatus } from "@/features/billing/types";
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Why: the feature and composition have `src/` as their CCF, but the type remains in the billing feature.
|
|
37
|
-
|
|
38
|
-
## Correct — Feature and Composition Type in `src/types/`
|
|
39
|
-
|
|
40
|
-
```text
|
|
41
|
-
src/
|
|
42
|
-
├── compositions/
|
|
43
|
-
│ └── BillingDashboard.tsx
|
|
44
|
-
├── features/
|
|
45
|
-
│ └── billing/
|
|
46
|
-
│ └── invoice.tsx
|
|
47
|
-
└── types/
|
|
48
|
-
├── billing.ts
|
|
49
|
-
└── index.ts
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
// src/types/billing.ts
|
|
54
|
-
export type InvoiceStatus = "draft" | "paid";
|
|
55
|
-
export type PaymentMethod = "card" | "bank";
|
|
56
|
-
|
|
57
|
-
// src/types/index.ts
|
|
58
|
-
export type { InvoiceStatus, PaymentMethod } from "./billing";
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
// src/features/billing/invoice.tsx
|
|
63
|
-
import type { InvoiceStatus } from "@/types";
|
|
64
|
-
|
|
65
|
-
// src/compositions/BillingDashboard.tsx
|
|
66
|
-
import type { InvoiceStatus } from "@/types";
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Why: the feature and composition have `src/` as their CCF, so the type lives in `src/types/`.
|
|
70
|
-
|
|
71
|
-
## Incorrect — Type Imported Independently from a Component
|
|
72
|
-
|
|
73
|
-
```tsx
|
|
74
|
-
// src/features/billing/invoice.tsx
|
|
75
|
-
export type DateRange = { from: Date; to: Date };
|
|
76
|
-
|
|
77
|
-
export function Invoice({ range }: { range: DateRange }): React.JSX.Element {
|
|
78
|
-
return <InvoiceView range={range} />;
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
// src/features/billing/hooks/use-billing-filter.ts
|
|
84
|
-
import type { DateRange } from "../invoice";
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Why: the hook reaches into a component file for a type it uses independently.
|
|
88
|
-
|
|
89
|
-
## Correct — Independently Used Type Extracted
|
|
90
|
-
|
|
91
|
-
```ts
|
|
92
|
-
// src/features/billing/types/index.ts
|
|
93
|
-
export type DateRange = { from: Date; to: Date };
|
|
94
|
-
|
|
95
|
-
// src/features/billing/hooks/use-billing-filter.ts
|
|
96
|
-
import { type DateRange } from "../types";
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Why: the component and hook can use the billing feature's types index independently.
|
|
100
|
-
|
|
101
|
-
## Incorrect — Schema Imported Independently from a Component
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
// src/features/billing/invoice-form.tsx
|
|
105
|
-
import { z } from "zod";
|
|
106
|
-
|
|
107
|
-
export const invoiceSchema = z.object({
|
|
108
|
-
amount: z.number().positive(),
|
|
109
|
-
});
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
// src/features/billing/hooks/use-invoice-draft.ts
|
|
114
|
-
import { invoiceSchema } from "../invoice-form";
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Why: the hook reaches into a component file for a schema it uses independently.
|
|
118
|
-
|
|
119
|
-
## Correct — Independently Used Schema Extracted
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
// src/features/billing/schemas/index.ts
|
|
123
|
-
import { z } from "zod";
|
|
124
|
-
|
|
125
|
-
export const invoiceSchema = z.object({
|
|
126
|
-
amount: z.number().positive(),
|
|
127
|
-
});
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
```tsx
|
|
131
|
-
// src/features/billing/invoice-form.tsx
|
|
132
|
-
import { invoiceSchema } from "./schemas";
|
|
133
|
-
|
|
134
|
-
// src/features/billing/hooks/use-invoice-draft.ts
|
|
135
|
-
import { invoiceSchema } from "../schemas";
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Why: the component and hook can use the billing feature's schemas index independently.
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Utilities Rule
|
|
2
|
-
|
|
3
|
-
Pure functions should not be hidden in component files. This rule extracts them to a predictable `utils/` folder and keeps their imports direct.
|
|
4
|
-
|
|
5
|
-
- A pure function MUST be extracted to `utils/`, even when it has one consumer.
|
|
6
|
-
- A pure function returns the same result for the same arguments and does not read or write external mutable state, perform I/O, or manipulate the DOM.
|
|
7
|
-
- A utility used only to implement one configuration object MUST live in that object's `utils/` folder.
|
|
8
|
-
- An extracted utility MUST live in the `utils/` folder at the closest common folder (CCF) of its consumers.
|
|
9
|
-
- When a utility's CCF is `src/features/`, it MUST move to `src/utils/`.
|
|
10
|
-
- A utility MUST be imported directly without a barrel.
|
|
11
|
-
- A utility file that exports one function MUST have a name in that function's kebab-case form.
|
|
12
|
-
- Utilities that are used together MAY be grouped in a file with a kebab-case name.
|
|
13
|
-
|
|
14
|
-
## Incorrect — Pure Function Left Beside Its Consumer
|
|
15
|
-
|
|
16
|
-
```tsx
|
|
17
|
-
// src/features/billing/invoice.tsx
|
|
18
|
-
const calculateInvoiceTotal = (items: InvoiceItem[]): number => {
|
|
19
|
-
return items.reduce((total, item) => total + item.amount, 0);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export function Invoice({ items }: InvoiceProps): React.JSX.Element {
|
|
23
|
-
const total = calculateInvoiceTotal(items);
|
|
24
|
-
return <InvoiceTotal value={total} />;
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Why: the pure function is mixed into a component file.
|
|
29
|
-
|
|
30
|
-
## Correct — Pure Function in a Directly Imported Utility File
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
// src/features/billing/utils/calculate-invoice-total.ts
|
|
34
|
-
export function calculateInvoiceTotal(items: InvoiceItem[]): number {
|
|
35
|
-
return items.reduce((total, item) => total + item.amount, 0);
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
```tsx
|
|
40
|
-
// src/features/billing/invoice.tsx
|
|
41
|
-
import { calculateInvoiceTotal } from "./utils/calculate-invoice-total";
|
|
42
|
-
|
|
43
|
-
export function Invoice({ items }: InvoiceProps): React.JSX.Element {
|
|
44
|
-
const total = calculateInvoiceTotal(items);
|
|
45
|
-
return <InvoiceTotal value={total} />;
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Why: the function has a focused utility file in the billing feature's `utils/` folder.
|
|
50
|
-
|
|
51
|
-
## Incorrect — Shared Utility Kept in a Feature
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
// src/features/billing/utils/format-retry-delay.ts
|
|
55
|
-
export function formatRetryDelay(milliseconds: number): string {
|
|
56
|
-
return `${milliseconds / 1000} seconds`;
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
```tsx
|
|
61
|
-
// src/features/billing/invoice.tsx
|
|
62
|
-
import { formatRetryDelay } from "./utils/format-retry-delay";
|
|
63
|
-
|
|
64
|
-
// src/compositions/billing-dashboard.tsx
|
|
65
|
-
import { formatRetryDelay } from "@/features/billing/utils/format-retry-delay";
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Why: the utility is imported by both a feature and a composition, so it cannot stay in the billing feature.
|
|
69
|
-
|
|
70
|
-
## Correct — Shared Utility in `src/utils/`
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
// src/utils/format-retry-delay.ts
|
|
74
|
-
export function formatRetryDelay(milliseconds: number): string {
|
|
75
|
-
return `${milliseconds / 1000} seconds`;
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
```tsx
|
|
80
|
-
// src/features/billing/invoice.tsx
|
|
81
|
-
import { formatRetryDelay } from "@/utils/format-retry-delay";
|
|
82
|
-
|
|
83
|
-
// src/compositions/billing-dashboard.tsx
|
|
84
|
-
import { formatRetryDelay } from "@/utils/format-retry-delay";
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Why: the utility's CCF is `src/`, so it lives in `src/utils/`.
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# [Topic] Guide
|
|
2
|
-
|
|
3
|
-
[1-2 short sentences explaining the underlying idea behind this guide and why it matters.]
|
|
4
|
-
|
|
5
|
-
## How To [Workflow]
|
|
6
|
-
|
|
7
|
-
[What does this accomplish? When should it run?]
|
|
8
|
-
|
|
9
|
-
1. [Step 1]
|
|
10
|
-
2. [Step 2]
|
|
11
|
-
|
|
12
|
-
## How To [Another Workflow]
|
|
13
|
-
|
|
14
|
-
[What does this accomplish? When should it run?]
|
|
15
|
-
|
|
16
|
-
1. [Step 1]
|
|
17
|
-
2. [Step 2]
|
|
18
|
-
|
|
19
|
-
[Repeat this section only when the guide has another real workflow. Otherwise delete it.]
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# [Topic] Reference
|
|
2
|
-
|
|
3
|
-
[1-2 short sentences naming exactly what readers can look up here and when they should use this reference.]
|
|
4
|
-
|
|
5
|
-
[Single lookup content — table, glossary, hierarchy, catalog, or config block. Keep this branch only when the reference has exactly one lookup block; it has no section heading. Otherwise delete this branch and use one heading per lookup group below.]
|
|
6
|
-
|
|
7
|
-
## [First Lookup Group Name]
|
|
8
|
-
|
|
9
|
-
[1-2 short sentences explaining what this group contains and why it is included.]
|
|
10
|
-
|
|
11
|
-
[Place this group's lookup content here.]
|
|
12
|
-
|
|
13
|
-
## [Next Lookup Group Name]
|
|
14
|
-
|
|
15
|
-
[1-2 short sentences explaining what this group contains and why it is included.]
|
|
16
|
-
|
|
17
|
-
[Place this group's lookup content here. Repeat the heading, explanation, and lookup content for every remaining group; delete this multi-block branch when the reference has one lookup block.]
|