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.
Files changed (72) hide show
  1. package/README.md +40 -55
  2. package/dist/eslint/pasika/index.d.ts +18 -0
  3. package/dist/eslint/pasika/index.js +21 -3
  4. package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +9 -0
  5. package/dist/eslint/pasika/rules/enforce-barrel-exports.js +78 -0
  6. package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +9 -0
  7. package/dist/eslint/pasika/rules/enforce-cn-merge.js +84 -0
  8. package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +9 -0
  9. package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +76 -0
  10. package/dist/eslint/pasika/rules/filename-case.d.ts +2 -0
  11. package/dist/eslint/pasika/rules/filename-case.js +100 -0
  12. package/dist/eslint/pasika/rules/import-boundaries.d.ts +2 -0
  13. package/dist/eslint/pasika/rules/{organization-imports.js → import-boundaries.js} +11 -1
  14. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +9 -0
  15. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +107 -0
  16. package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +9 -0
  17. package/dist/eslint/pasika/rules/no-mixed-concerns.js +84 -0
  18. package/package.json +5 -16
  19. package/claude/hooks/.vulyk +0 -3
  20. package/claude/hooks/AGENTS.md +0 -3
  21. package/claude/hooks/CLAUDE.md +0 -1
  22. package/claude/hooks/claude-hooks.md +0 -30
  23. package/claude/hooks/notification.sh +0 -38
  24. package/claude/hooks/protect-files.sh +0 -21
  25. package/claude/hooks/status-line/index.js +0 -57
  26. package/claude/scripts/render-settings.ts +0 -223
  27. package/claude/settings.base.json +0 -38
  28. package/dist/claude/scripts/render-settings.js +0 -145
  29. package/dist/eslint/pasika/rules/organization-imports.d.ts +0 -2
  30. package/dist/eslint.config.js +0 -7
  31. package/dist/scripts/pasika.js +0 -59
  32. package/docs/agent-conventions.md +0 -21
  33. package/docs/claude/hooks.md +0 -30
  34. package/docs/code-organization-guide/code-organization-guide.md +0 -65
  35. package/docs/code-organization-guide/references/application-architecture-reference.md +0 -107
  36. package/docs/code-organization-guide/rules/component-placement-rule.md +0 -135
  37. package/docs/code-organization-guide/rules/configuration-rule.md +0 -42
  38. package/docs/code-organization-guide/rules/constants-rule.md +0 -77
  39. package/docs/code-organization-guide/rules/exports-and-imports-rule.md +0 -81
  40. package/docs/code-organization-guide/rules/folder-nesting-rule.md +0 -83
  41. package/docs/code-organization-guide/rules/hook-extraction-rule.md +0 -142
  42. package/docs/code-organization-guide/rules/interactive-component-rule.md +0 -100
  43. package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +0 -68
  44. package/docs/code-organization-guide/rules/locales-rule.md +0 -55
  45. package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +0 -66
  46. package/docs/code-organization-guide/rules/native-prop-forwarding-rule.md +0 -38
  47. package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +0 -64
  48. package/docs/code-organization-guide/rules/repeated-structure-rule.md +0 -93
  49. package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +0 -116
  50. package/docs/code-organization-guide/rules/sole-state-owner-rule.md +0 -102
  51. package/docs/code-organization-guide/rules/types-and-schemas-rule.md +0 -138
  52. package/docs/code-organization-guide/rules/utilities-rule.md +0 -87
  53. package/docs/documentation-guide/_templates/guide.md +0 -19
  54. package/docs/documentation-guide/_templates/reference.md +0 -17
  55. package/docs/documentation-guide/_templates/rule.md +0 -21
  56. package/docs/documentation-guide/documentation-guide.md +0 -13
  57. package/docs/documentation-guide/references/documentation-types-reference.md +0 -9
  58. package/docs/documentation-guide/rules/guide-creation-rule.md +0 -112
  59. package/docs/documentation-guide/rules/reference-creation-rule.md +0 -132
  60. package/docs/documentation-guide/rules/rule-creation-rule.md +0 -81
  61. package/docs/documentation-guide/rules/template-usage-rule.md +0 -48
  62. package/docs/shadcn-theme.md +0 -121
  63. package/docs/styling-guide/rules/class-composition-rule.md +0 -32
  64. package/docs/styling-guide/rules/color-role-naming-rule.md +0 -115
  65. package/docs/styling-guide/rules/component-state-rule.md +0 -26
  66. package/docs/styling-guide/rules/component-variant-rule.md +0 -128
  67. package/docs/styling-guide/rules/global-style-system-rule.md +0 -63
  68. package/docs/styling-guide/rules/style-placement-rule.md +0 -49
  69. package/docs/styling-guide/rules/tailwind-utility-rule.md +0 -48
  70. package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +0 -129
  71. package/docs/styling-guide/rules/theme-token-rule.md +0 -32
  72. package/docs/styling-guide/styling-guide.md +0 -17
@@ -1,77 +0,0 @@
1
- # Constants Rule
2
-
3
- Duplicated constants are hard to keep in sync, while extracting every single-use value creates unnecessary files. This rule keeps reused constants in one module and leaves single-use values close to their consumer.
4
-
5
- - A constant is an exported or reused fixed domain value; local literals and implementation values are not constants for this rule. A configuration object is not a constant for this rule.
6
- - Except for a constant used only to implement one configuration object, a constant MUST stay in the file that uses it until another file imports it.
7
- - A constant used only to implement one configuration object MUST live in that object's `constants/` folder.
8
- - Extracted constants MUST live in a `constants/` folder at the closest common folder (CCF) of their consumers.
9
- - A `constants/index.ts` MAY define its exports directly.
10
- - Consumers MUST import extracted constants through the `constants/index.ts` at their scope.
11
- - Constants that are used together MAY be grouped in a file with a kebab-case name and named-re-exported from `constants/index.ts`.
12
- - When a `constants/` folder uses grouped files, its `index.ts` MUST only named-re-export those files.
13
- - When a constant's CCF is `src/features/`, it MUST move to `src/constants/`.
14
-
15
- ## Incorrect — Constant Imported Without `constants/index.ts`
16
-
17
- ```ts
18
- // src/features/billing/constants/max-retries.ts
19
- export const maxRetries = 3;
20
- ```
21
-
22
- ```ts
23
- // src/features/billing/hooks/use-retry-payment.ts
24
- import { maxRetries } from "../constants/max-retries";
25
- ```
26
-
27
- Why: the consumer bypasses the feature's `constants/index.ts`.
28
-
29
- ## Correct — Grouped Constants Re-Exported from `constants/index.ts`
30
-
31
- ```ts
32
- // src/features/billing/constants/retry.ts
33
- export const maxRetries = 3;
34
- export const retryDelayMs = 1_000;
35
-
36
- // src/features/billing/constants/index.ts
37
- export { maxRetries, retryDelayMs } from "./retry";
38
- ```
39
-
40
- ```ts
41
- // src/features/billing/hooks/use-retry-payment.ts
42
- import { maxRetries } from "../constants";
43
- ```
44
-
45
- Why: the constants are available through the feature's `constants/index.ts`.
46
-
47
- ## Incorrect — Feature and Composition Constant Kept in a Feature
48
-
49
- ```ts
50
- // src/features/billing/constants/index.ts
51
- export const paymentRetryDelayMs = 1_000;
52
-
53
- // src/features/billing/hooks/use-retry-payment.ts
54
- import { paymentRetryDelayMs } from "../constants";
55
-
56
- // src/compositions/BillingDashboard.tsx
57
- import { paymentRetryDelayMs } from "@/features/billing/constants";
58
- ```
59
-
60
- Why: the feature and composition have `src/` as their CCF, but the constant remains in the billing feature.
61
-
62
- ## Correct — Feature and Composition Constant in `src/constants/`
63
-
64
- ```ts
65
- // src/constants/index.ts
66
- export const paymentRetryDelayMs = 1_000;
67
- ```
68
-
69
- ```tsx
70
- // src/features/billing/hooks/use-retry-payment.ts
71
- import { paymentRetryDelayMs } from "@/constants";
72
-
73
- // src/compositions/BillingDashboard.tsx
74
- import { paymentRetryDelayMs } from "@/constants";
75
- ```
76
-
77
- Why: the feature and composition have `src/` as their CCF, so the constant lives in `src/constants/`.
@@ -1,81 +0,0 @@
1
- # Exports and Imports Rule
2
-
3
- Without consistent export and import styles, it is harder to tell what a file contains and how other files should import from it. This rule gives each file type a predictable export style and keeps import paths consistent.
4
-
5
- - Static JavaScript and TypeScript modules under `src/` MUST use named exports unless a framework or third-party package requires a different export style for a specific file.
6
- - Static imports under `src/` MUST use relative paths for the same folder, a direct subfolder, or one folder up.
7
- - Static imports under `src/` MUST use the `@/*` alias for anything beyond one folder up.
8
- - ESLint MUST enforce this rule's export and static import restrictions under `src/`.
9
-
10
- ## Incorrect — Single-Export Utility Uses a Default Export
11
-
12
- ```ts
13
- // src/utils/format-duration.ts
14
- export default function formatDuration(seconds: number): string {
15
- const minutes = Math.floor(seconds / 60);
16
- const remainder = seconds % 60;
17
- return `${minutes}:${String(remainder).padStart(2, "0")}`;
18
- }
19
- ```
20
-
21
- Why: files use named exports even when they expose one item.
22
-
23
- ## Correct — Single-Export Utility Uses a Named Export
24
-
25
- ```ts
26
- // src/utils/format-duration.ts
27
- export function formatDuration(seconds: number): string {
28
- const minutes = Math.floor(seconds / 60);
29
- const remainder = seconds % 60;
30
- return `${minutes}:${String(remainder).padStart(2, "0")}`;
31
- }
32
- ```
33
-
34
- Why: the utility uses the same named-export form as every other file that does not have a package-required export contract.
35
-
36
- ## Incorrect — Deep Relative Path
37
-
38
- ```ts
39
- // src/features/stream/StreamBoard/schedule.ts
40
- import { debounce } from "../../../utils/debounce";
41
- ```
42
-
43
- Why: reaching more than one folder up hides the cross-scope dependency in relative path traversal.
44
-
45
- ## Correct — Alias Across Scopes and Relative Paths Nearby
46
-
47
- ```ts
48
- // src/features/stream/StreamBoard/schedule.ts
49
- import { debounce } from "@/utils/debounce";
50
- import { formatSlot } from "./utils/format-slot";
51
- import { type StreamSlot } from "./types";
52
- import { buildSchedule } from "../schedule-builder";
53
- ```
54
-
55
- Why: the alias identifies the distant dependency while nearby files keep short relative paths.
56
-
57
- ## Incorrect — Next.js Page Uses a Named Export
58
-
59
- ```tsx
60
- // src/app/contact/page.tsx
61
- import { locales } from "@/locales";
62
-
63
- export function Page(): React.JSX.Element {
64
- return <main>{locales.contactUs}</main>;
65
- }
66
- ```
67
-
68
- Why: Next.js requires a `page.tsx` file to default-export its page component.
69
-
70
- ## Correct — Next.js Page Uses a Default Export
71
-
72
- ```tsx
73
- // src/app/contact/page.tsx
74
- import { locales } from "@/locales";
75
-
76
- export default function Page(): React.JSX.Element {
77
- return <main>{locales.contactUs}</main>;
78
- }
79
- ```
80
-
81
- Why: the page follows the export contract Next.js requires.
@@ -1,83 +0,0 @@
1
- # Folder Nesting Rule
2
-
3
- Without nesting, exclusive children can look reusable and their relationship to the parent is easy to miss in review. This rule groups them with their parent and keeps them out of the folder's public API.
4
-
5
- - An exclusive child component is imported only by its parent component or descendants in the parent's folder. A flat component MUST be nested in a folder with the same name when it gains one or more exclusive child components.
6
- - A child that gains a consumer outside its parent's folder MUST move according to the Component Placement Rule, including that rule's CCF consumer exclusions.
7
- - A component MUST NOT be nested only because it has support files.
8
- - A nested component's support files MUST live in its folder.
9
- - The nested folder's `index.ts` MUST named-re-export the nested component and MUST NOT re-export its exclusive children.
10
-
11
- ## Incorrect — Exclusive Children Kept Flat
12
-
13
- ```text
14
- src/features/blog/
15
- BlogPage.tsx # owns exclusive children
16
- blog-header.tsx # exclusive child — no sibling needs it
17
- blog-footer.tsx # exclusive child — no sibling needs it
18
- hooks/
19
- use-blog-filter.ts # used only by BlogPage
20
- ```
21
-
22
- Why: `BlogPage` owns children that no sibling uses, but all three files sit as flat siblings. Nothing in the tree shows that `blog-header.tsx` and `blog-footer.tsx` belong to `BlogPage` rather than to any other component in the folder.
23
-
24
- ## Correct — Exclusive Children Nested
25
-
26
- ```text
27
- src/features/blog/
28
- BlogPage/
29
- index.ts # re-exports only BlogPage.tsx
30
- BlogPage.tsx
31
- blog-header.tsx # not re-exported from index.ts
32
- blog-footer.tsx # not re-exported from index.ts
33
- hooks/
34
- use-blog-filter.ts
35
- ```
36
-
37
- Why: nesting `BlogPage` into `BlogPage/` makes the parent–child relationship visible in the filesystem, lets the children stay scoped to their concrete consumer, and gives the folder a barrel that re-exports only the nested component.
38
-
39
- ## Incorrect — Support Files Cause Unnecessary Nesting
40
-
41
- ```text
42
- src/features/blog/
43
- BlogPage/
44
- index.ts
45
- BlogPage.tsx
46
- hooks/
47
- use-blog-filter.ts
48
- ```
49
-
50
- Why: support files alone do not make `BlogPage` a nested component.
51
-
52
- ## Correct — Support Files Keep a Component Flat
53
-
54
- ```text
55
- src/features/blog/
56
- BlogPage.tsx
57
- hooks/
58
- use-blog-filter.ts
59
- ```
60
-
61
- Why: without exclusive children, `BlogPage` stays flat and its support files remain at the feature scope.
62
-
63
- ## Incorrect — Exclusive Child Re-Exported
64
-
65
- ```ts
66
- // src/features/blog/BlogPage/index.ts
67
- export { BlogPage } from "./BlogPage";
68
- export { BlogHeader } from "./blog-header";
69
- ```
70
-
71
- Why: the barrel re-exports the child as well as the nested component, so outside consumers can import `blog-header.tsx` through `index.ts` and the child stops being exclusive to `BlogPage`.
72
-
73
- ## Correct — Only the Nested Component Re-Exported
74
-
75
- ```tsx
76
- // src/features/blog/BlogPage/index.ts
77
- export { BlogPage } from "./BlogPage";
78
-
79
- // src/features/blog/BlogPage/BlogPage.tsx
80
- import { BlogHeader } from "./blog-header";
81
- ```
82
-
83
- Why: the barrel exposes only the nested component, so it does not expose the child.
@@ -1,142 +0,0 @@
1
- # Hook Extraction Rule
2
-
3
- Keeping every hook inline makes components bloated, while extracting every hook adds indirection without benefit. This rule defines concrete reuse and imperative-complexity triggers for extraction.
4
-
5
- - A custom hook MUST be extracted to its own file when two or more consumers use it.
6
- - A custom hook used only to implement one configuration object MUST live in that object's `hooks/` folder.
7
- - A custom hook with exactly one consumer MUST be extracted when it contains two or more imperative categories and can be described as one coherent behavior.
8
- - An extracted custom hook MUST live in a `hooks/` folder at the closest common folder (CCF) of its consumers.
9
- - When a custom hook's CCF is `src/features/`, it MUST move to `src/hooks/`.
10
- - The imperative categories MUST be subscriptions, external I/O and persistence, DOM manipulation, or resource lifecycle.
11
- - One operation MAY count in only one imperative category; use its primary purpose when categories overlap.
12
- - Subscriptions MUST include event listeners and registration or cleanup APIs such as `on()` and `off()`.
13
- - External I/O and persistence MUST include network requests, asynchronous reads or writes, and browser storage.
14
- - DOM manipulation MUST include imperative APIs such as `focus()`, `classList`, observers, or imperative rendering.
15
- - Resource lifecycle MUST include setup and teardown APIs such as `load()`, `destroy()`, or `dispose()`.
16
- - A custom hook with one consumer that does not meet the two-category threshold MUST stay inline in its consumer module, unless it is used only to implement one configuration object.
17
-
18
- ## Incorrect — Two Imperative Categories Left Inline
19
-
20
- ```tsx
21
- // src/features/player/Player.tsx
22
- export function Player({ src }: PlayerProps): React.JSX.Element {
23
- useEffect(() => {
24
- player.on("play", handlePlay);
25
- player.on("pause", handlePause);
26
- player.load(src);
27
-
28
- return () => {
29
- player.off("play", handlePlay);
30
- player.off("pause", handlePause);
31
- player.destroy();
32
- };
33
- }, [src]);
34
-
35
- return <PlayerView />;
36
- }
37
- ```
38
-
39
- Why: one coherent player-setup behavior combines subscriptions with resource lifecycle, so leaving it inline crosses the two-category threshold.
40
-
41
- ## Correct — Complex Single-Use Hook Extracted
42
-
43
- ```ts
44
- // src/features/player/hooks/use-player-setup.ts
45
- export function usePlayerSetup(src: string): void {
46
- useEffect(() => {
47
- player.on("play", handlePlay);
48
- player.on("pause", handlePause);
49
- player.load(src);
50
-
51
- return () => {
52
- player.off("play", handlePlay);
53
- player.off("pause", handlePause);
54
- player.destroy();
55
- };
56
- }, [src]);
57
- }
58
- ```
59
-
60
- ```tsx
61
- // src/features/player/Player.tsx
62
- import { usePlayerSetup } from "./hooks/use-player-setup";
63
-
64
- export function Player({ src }: PlayerProps): React.JSX.Element {
65
- usePlayerSetup(src);
66
- return <PlayerView />;
67
- }
68
- ```
69
-
70
- Why: the named hook owns the subscription and resource lifecycle for one coherent behavior, keeping the component focused on rendering.
71
-
72
- ## Incorrect — Reused Hook Kept Inline
73
-
74
- ```tsx
75
- // src/features/billing/invoice.tsx
76
- const useInvoiceSort = (invoices: Invoice[]): Invoice[] => {
77
- return useMemo(() => invoices.toSorted(byDate), [invoices]);
78
- };
79
-
80
- // src/features/billing/invoice-summary.tsx
81
- const useInvoiceSort = (invoices: Invoice[]): Invoice[] => {
82
- return useMemo(() => invoices.toSorted(byDate), [invoices]);
83
- };
84
- ```
85
-
86
- Why: two components use the same hook behavior, so keeping it inline duplicates it.
87
-
88
- ## Correct — Reused Hook Extracted
89
-
90
- ```ts
91
- // src/features/billing/hooks/use-invoice-sort.ts
92
- export function useInvoiceSort(invoices: Invoice[]): Invoice[] {
93
- return useMemo(() => invoices.toSorted(byDate), [invoices]);
94
- }
95
- ```
96
-
97
- ```tsx
98
- // src/features/billing/invoice.tsx
99
- import { useInvoiceSort } from "./hooks/use-invoice-sort";
100
-
101
- // src/features/billing/invoice-summary.tsx
102
- import { useInvoiceSort } from "./hooks/use-invoice-sort";
103
- ```
104
-
105
- Why: the hook has two consumers, so it has its own file.
106
-
107
- ## Incorrect — Simple Single-Use Hook Extracted
108
-
109
- ```ts
110
- // src/features/billing/hooks/use-invoice-sort.ts
111
- export function useInvoiceSort(invoices: Invoice[]): Invoice[] {
112
- return useMemo(() => invoices.toSorted(byDate), [invoices]);
113
- }
114
- ```
115
-
116
- ```tsx
117
- // src/features/billing/invoice.tsx
118
- import { useInvoiceSort } from "./hooks/use-invoice-sort";
119
-
120
- export function Invoice({ invoices }: InvoiceProps): React.JSX.Element {
121
- const sortedInvoices = useInvoiceSort(invoices);
122
- return <InvoiceList invoices={sortedInvoices} />;
123
- }
124
- ```
125
-
126
- Why: the hook has one consumer and no imperative category, so its separate file adds indirection before an extraction trigger exists.
127
-
128
- ## Correct — Simple Single-Use Hook Inline
129
-
130
- ```tsx
131
- // src/features/billing/invoice.tsx
132
- const useInvoiceSort = (invoices: Invoice[]): Invoice[] => {
133
- return useMemo(() => invoices.toSorted(byDate), [invoices]);
134
- };
135
-
136
- export function Invoice({ invoices }: InvoiceProps): React.JSX.Element {
137
- const sortedInvoices = useInvoiceSort(invoices);
138
- return <InvoiceList invoices={sortedInvoices} />;
139
- }
140
- ```
141
-
142
- Why: the hook stays beside its sole consumer until reuse or imperative complexity provides a mechanical extraction trigger.
@@ -1,100 +0,0 @@
1
- # Interactive Component Rule
2
-
3
- Large component files need a clear way to decide what to extract first. This rule treats interactive elements as meaningful component boundaries instead of extracting arbitrary layout elements.
4
-
5
- - An [interactive HTML element](https://html.spec.whatwg.org/multipage/dom.html#interactive-content) MUST be extracted to a component with a descriptive name unless it is the top-level native element returned by the component that renders it in every rendered result.
6
-
7
- ## Incorrect — Interactive Element Kept Inline
8
-
9
- ```tsx
10
- // src/features/layout/header-section.tsx
11
- import { locales } from "@/locales";
12
-
13
- export function HeaderSection({
14
- onMenuClick,
15
- searchPlaceholder,
16
- }: {
17
- onMenuClick: () => void;
18
- searchPlaceholder: string;
19
- }): React.JSX.Element {
20
- return (
21
- <header className="flex items-center justify-between px-6 py-4">
22
- <h1 className="text-2xl">{locales.layout.headerTitle}</h1>
23
-
24
- <button onClick={onMenuClick} aria-label={locales.layout.openMenu}>
25
- <Icon name="menu" />
26
- </button>
27
-
28
- <input type="search" placeholder={searchPlaceholder} />
29
- </header>
30
- );
31
- }
32
- ```
33
-
34
- Why: the interactive elements remain mixed into `HeaderSection` instead of having their own components.
35
-
36
- ## Correct — Interactive Element Extracted
37
-
38
- ```text
39
- src/features/layout/
40
- header-section/
41
- index.ts # re-exports only header-section.tsx
42
- header-section.tsx
43
- menu-button.tsx # exclusive child — imported directly
44
- search-field.tsx # exclusive child — imported directly
45
- ```
46
-
47
- ```tsx
48
- // src/features/layout/header-section/menu-button.tsx
49
- type MenuButtonProps = React.ComponentProps<"button">;
50
-
51
- export function MenuButton({ onClick, ...props }: MenuButtonProps): React.JSX.Element {
52
- return (
53
- <button {...props} onClick={onClick}>
54
- <Icon name="menu" />
55
- </button>
56
- );
57
- }
58
- ```
59
-
60
- ```tsx
61
- // src/features/layout/header-section/search-field.tsx
62
- type SearchFieldProps = Omit<React.ComponentProps<"input">, "type">;
63
-
64
- export function SearchField(props: SearchFieldProps): React.JSX.Element {
65
- return <input {...props} type="search" />;
66
- }
67
- ```
68
-
69
- ```tsx
70
- // src/features/layout/header-section/header-section.tsx — now imports both interactive units instead of inline JSX
71
-
72
- import { MenuButton } from "./menu-button";
73
- import { SearchField } from "./search-field";
74
- import { cn } from "@/utils/cn";
75
- import { locales } from "@/locales";
76
-
77
- type HeaderSectionProps = React.ComponentProps<"header"> & {
78
- onMenuClick: () => void;
79
- searchPlaceholder: string;
80
- };
81
-
82
- export function HeaderSection({
83
- className,
84
- onMenuClick,
85
- searchPlaceholder,
86
- ...props
87
- }: HeaderSectionProps): React.JSX.Element {
88
- return (
89
- <header {...props} className={cn("flex items-center justify-between px-6 py-4", className)}>
90
- <h1 className="text-2xl">{locales.layout.headerTitle}</h1>
91
-
92
- <MenuButton onClick={onMenuClick} aria-label={locales.layout.openMenu} />
93
-
94
- <SearchField placeholder={searchPlaceholder} />
95
- </header>
96
- );
97
- }
98
- ```
99
-
100
- Why: each interactive element now has its own descriptive component and forwards native props, leaving `HeaderSection` to compose them.
@@ -1,68 +0,0 @@
1
- # JSX Hygiene Rule
2
-
3
- JSX should show the component's structure, not its calculations. This rule moves complex expressions before `return` while keeping simple JSX readable.
4
-
5
- - Arithmetic, chained built-in method calls, calls to functions declared outside the component, nested ternaries, and conditions containing two or more logical operators MUST be extracted before `return`, including in JSX attributes.
6
- - An inline expression MAY contain one condition with at most one logical operator, one ternary, or one built-in method call. `cn()` is an explicit exception to the external-function-call restriction. Calls inside an event-handler callback are not JSX expressions.
7
-
8
- ## Incorrect — Computation in JSX
9
-
10
- ```tsx
11
- return (
12
- <div>
13
- <span>{Math.floor((Date.now() - new Date(record.updatedAt).getTime()) / 86400000)} {locales.daysAgo}</span>
14
- <ul>
15
- {items
16
- .filter((x) => x.active)
17
- .sort(byDate)
18
- .map(renderItem)}
19
- </ul>
20
- <p>
21
- {formatDate(record.updatedAt, "long")} — {calculateTotal(items)}
22
- </p>
23
- {isLoading ? <Spinner /> : hasError ? <Error /> : <Content />}
24
- {isLoggedIn && hasPermission && isOwner && featureEnabled && <AdminPanel />}
25
- <span>{new Date(post.publishedAt).toLocaleDateString()}</span>
26
- {Math.round(score * 100)}%
27
- </div>
28
- );
29
- ```
30
-
31
- Why: calculations, chained methods, function calls, nested ternaries, and long guards all belong outside the return.
32
-
33
- ## Correct — Computation Before `return`
34
-
35
- ```tsx
36
- const daysSinceUpdate = Math.floor((Date.now() - new Date(record.updatedAt).getTime()) / 86400000);
37
- const activeItems = items.filter((x) => x.active).sort(byDate);
38
- const updatedLabel = formatDate(record.updatedAt, "long");
39
- const total = calculateTotal(items);
40
- const canShowAdmin = isLoggedIn && hasPermission && isOwner && featureEnabled;
41
- const publishDate = new Date(post.publishedAt).toLocaleDateString();
42
- const scorePercent = Math.round(score * 100);
43
- const daysAgoLabel = locales.daysAgo;
44
-
45
- let statusView = <Content />;
46
- if (isLoading) {
47
- statusView = <Spinner />;
48
- } else if (hasError) {
49
- statusView = <Error />;
50
- }
51
-
52
- return (
53
- <div>
54
- <span>{daysSinceUpdate} {daysAgoLabel}</span>
55
- <ul>{activeItems.map(renderItem)}</ul>
56
- <p>
57
- {updatedLabel} — {total}
58
- </p>
59
- {statusView}
60
- {scorePercent}%
61
- <div className={cn("base", isActive && "bg-primary-100")} />
62
- {canShowAdmin && <AdminPanel />}
63
- <span>{publishDate}</span>
64
- </div>
65
- );
66
- ```
67
-
68
- Why: calculations, chained methods, custom function calls, and nested ternaries now resolve before the return, so only simple conditions, single method calls, and `cn()` stay inline.
@@ -1,55 +0,0 @@
1
- # Locales Rule
2
-
3
- When locale strings are scattered across components and constants, they are hard to find and keep consistent. This rule keeps them in one central file, puts each feature's strings together, and uses readable keys.
4
-
5
- - All locales MUST live in the named `locales` object exported from `src/locales/index.ts`.
6
- - A locale consumer is a module that reads the locale.
7
- - A new locale with no consumers MUST use a feature namespace when it is for one feature; otherwise it MUST live at the top level of `locales`.
8
- - Locales read only by modules in one feature folder MUST live in an object with the camelCase form of its feature folder name (for example, `user-settings` becomes `userSettings`).
9
- - Locales read by modules in more than one feature folder or by `src/shared/`, `src/compositions/`, `src/app/`, or root support folders MUST live at the top level of `locales`.
10
- - A namespaced locale MUST be read through its full dotted path (`locales.stream.watchLiveStream`).
11
- - A locale key MUST be camelCase English based on the text, unless a direct translation would be unclear or unwieldy. In that case, it MAY describe the message's purpose instead.
12
-
13
- ## Incorrect — Flat Feature Locale Keys
14
-
15
- ```ts
16
- // src/locales/index.ts
17
- export const locales = {
18
- streamPlayerLiveText: "Дивитись прямий ефір", // describes the element, not the text
19
- ctaButton: "Прийняти всі cookies", // describes the element, not the text
20
- youSuccessfullySubscribedToUpdates: "Ви успішно підписалися на оновлення", // direct translation — unwieldy
21
- };
22
- ```
23
-
24
- ```tsx
25
- // src/features/stream/stream-player.tsx
26
- locales.streamPlayerLiveText; // no namespace — collides with any other feature that picks this key
27
- ```
28
-
29
- Why: the first two keys describe elements instead of text, the direct translation is unwieldy, and the flat structure does not show that `streamPlayerLiveText` belongs to the stream feature.
30
-
31
- ## Correct — Namespaced Feature Locale Keys
32
-
33
- ```ts
34
- // src/locales/index.ts
35
- export const locales = {
36
- stream: {
37
- watchLiveStream: "Дивитись прямий ефір",
38
- },
39
- acceptAllCookies: "Прийняти всі cookies", // shared — direct text-based key
40
- subscriptionConfirmed: "Ви успішно підписалися на оновлення", // shared — describes the message's purpose
41
- };
42
- ```
43
-
44
- ```tsx
45
- // src/features/stream/stream-player.tsx
46
- locales.stream.watchLiveStream; // namespaced by feature
47
-
48
- // src/shared/cookie-banner.tsx
49
- locales.acceptAllCookies; // shared — read from the top level
50
-
51
- // src/shared/subscription-form.tsx
52
- locales.subscriptionConfirmed; // shared — read from the top level
53
- ```
54
-
55
- Why: the stream feature has its own namespace, while shared strings stay at the top level. `acceptAllCookies` is based on the text, while `subscriptionConfirmed` describes the message's purpose instead of using an unwieldy direct translation.
@@ -1,66 +0,0 @@
1
- # Nameable Visual Concept Rule
2
-
3
- Some groups of elements can be given a clear component name but have no file of their own. This rule recommends extracting those groups into descriptive components.
4
-
5
- - A block of elements SHOULD be extracted to a component when one clear name describes the whole block.
6
-
7
- ## Incorrect — Nameable Visual Block Kept Inline
8
-
9
- ```tsx
10
- // src/features/feed/feed-view.tsx
11
- export function FeedView(): React.JSX.Element {
12
- return (
13
- <main>
14
- <article>
15
- <Avatar />
16
- <UserName />
17
- <PostTimestamp />
18
- <PostBody />
19
- </article>
20
- </main>
21
- );
22
- }
23
- ```
24
-
25
- Why: the avatar, username, and timestamp form a message header but remain inline in `FeedView`.
26
-
27
- ## Correct — Nameable Visual Block Extracted
28
-
29
- ```text
30
- src/features/feed/
31
- feed-view/
32
- index.ts # re-exports only feed-view.tsx
33
- feed-view.tsx
34
- message-header.tsx # exclusive child — imported directly by feed-view.tsx
35
- ```
36
-
37
- ```tsx
38
- // src/features/feed/feed-view/feed-view.tsx
39
- import { MessageHeader } from "./message-header";
40
-
41
- export function FeedView(): React.JSX.Element {
42
- return (
43
- <main>
44
- <article>
45
- <MessageHeader />
46
- <PostBody />
47
- </article>
48
- </main>
49
- );
50
- }
51
- ```
52
-
53
- ```tsx
54
- // src/features/feed/feed-view/message-header.tsx
55
- export function MessageHeader(): React.JSX.Element {
56
- return (
57
- <header>
58
- <Avatar />
59
- <UserName />
60
- <PostTimestamp />
61
- </header>
62
- );
63
- }
64
- ```
65
-
66
- Why: `MessageHeader` gives the group a clear name and its own file, while `FeedView` only composes it.