@tailor-platform/erp-kit 0.1.0 → 0.1.1

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 (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +64 -44
  4. package/package.json +4 -2
  5. package/skills/1-module-docs/SKILL.md +4 -0
  6. package/{rules/module-development → skills/1-module-docs/references}/structure.md +2 -7
  7. package/skills/2-module-feature-breakdown/SKILL.md +6 -0
  8. package/{rules/module-development → skills/2-module-feature-breakdown/references}/commands.md +0 -6
  9. package/{rules/module-development → skills/2-module-feature-breakdown/references}/models.md +0 -5
  10. package/skills/2-module-feature-breakdown/references/structure.md +22 -0
  11. package/skills/3-module-doc-review/SKILL.md +6 -0
  12. package/skills/3-module-doc-review/references/commands.md +54 -0
  13. package/skills/3-module-doc-review/references/models.md +29 -0
  14. package/{rules/module-development → skills/3-module-doc-review/references}/testing.md +0 -6
  15. package/skills/4-module-tdd-implementation/SKILL.md +24 -6
  16. package/skills/4-module-tdd-implementation/references/commands.md +45 -0
  17. package/{rules/sdk-best-practices → skills/4-module-tdd-implementation/references}/db-relations.md +0 -5
  18. package/{rules/module-development → skills/4-module-tdd-implementation/references}/errors.md +0 -5
  19. package/{rules/module-development → skills/4-module-tdd-implementation/references}/exports.md +0 -5
  20. package/skills/4-module-tdd-implementation/references/models.md +30 -0
  21. package/skills/4-module-tdd-implementation/references/structure.md +22 -0
  22. package/skills/4-module-tdd-implementation/references/testing.md +37 -0
  23. package/skills/5-module-implementation-review/SKILL.md +8 -0
  24. package/skills/5-module-implementation-review/references/commands.md +45 -0
  25. package/skills/5-module-implementation-review/references/errors.md +7 -0
  26. package/skills/5-module-implementation-review/references/exports.md +8 -0
  27. package/skills/5-module-implementation-review/references/models.md +30 -0
  28. package/skills/5-module-implementation-review/references/testing.md +29 -0
  29. package/skills/app-compose-1-requirement-analysis/SKILL.md +4 -0
  30. package/{rules/app-compose → skills/app-compose-1-requirement-analysis/references}/structure.md +0 -5
  31. package/skills/app-compose-2-requirements-breakdown/SKILL.md +7 -0
  32. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-detailview.md +0 -6
  33. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-form.md +0 -6
  34. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-listview.md +0 -6
  35. package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
  36. package/skills/app-compose-3-doc-review/SKILL.md +4 -0
  37. package/skills/app-compose-3-doc-review/references/structure.md +27 -0
  38. package/skills/app-compose-4-design-mock/SKILL.md +8 -0
  39. package/{rules/app-compose/frontend → skills/app-compose-4-design-mock/references}/component.md +0 -5
  40. package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
  41. package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
  42. package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
  43. package/skills/app-compose-4-design-mock/references/structure.md +27 -0
  44. package/skills/app-compose-5-design-mock-review/SKILL.md +7 -0
  45. package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
  46. package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
  47. package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
  48. package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
  49. package/skills/app-compose-6-implementation-spec/SKILL.md +5 -0
  50. package/{rules/app-compose/backend → skills/app-compose-6-implementation-spec/references}/auth.md +0 -6
  51. package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
  52. package/src/cli.ts +2 -2
  53. package/src/commands/init.test.ts +30 -19
  54. package/src/commands/init.ts +76 -43
  55. package/rules/app-compose/frontend/auth.md +0 -55
  56. package/rules/app-compose/frontend/page.md +0 -86
  57. package/rules/module-development/cross-module-type-injection.md +0 -28
  58. package/rules/module-development/dependency-modules.md +0 -24
  59. package/rules/module-development/executors.md +0 -67
  60. package/rules/module-development/sync-vs-async-operations.md +0 -83
  61. package/rules/sdk-best-practices/sdk-docs.md +0 -14
@@ -0,0 +1,45 @@
1
+ # Command Implementation
2
+
3
+ ## Dual Function Pattern
4
+
5
+ Two functions per operation: internal `_fn` and public `fn`.
6
+
7
+ **Internal function (`_myFunction`):**
8
+
9
+ - Takes concrete `DB` type from `generated/kysely-tailordb`
10
+ - Return type uses `Schema` from `lib/types`: `Promise<{ entity: Entity<Schema> }>`
11
+ - Contains actual implementation
12
+
13
+ **Public function (`myFunction`):**
14
+
15
+ - Generic signature: `<T extends { Entity: object }>(db: Kysely<T>, ...)`
16
+ - Return type uses generic `T`: `Promise<{ entity: Entity<T> }>`
17
+ - Delegates to internal with type casting: `_fn(db as unknown as DB, ...) as unknown as Result`
18
+
19
+ ## Implementation Considerations
20
+
21
+ - **Validation**: Check referenced entities exist before operating
22
+ - **Idempotency**: For assign/revoke, return existing instead of throwing
23
+ - **Return format**: Wrap in object `{ entity }` not just `entity`
24
+
25
+ ## Conventions
26
+
27
+ - Input types: exported interfaces (`export interface MyFunctionInput`)
28
+ - Use `.executeTakeFirst()` for single results
29
+ - Include JSDoc: `/** Function: name \n Description */`
30
+
31
+ ## State Transitions
32
+
33
+ For commands that transition between statuses, accept `from?: string[]` with a default:
34
+
35
+ ```typescript
36
+ from?: string[]; // Default: ["ACTIVE"]
37
+
38
+ const validFromStatuses = input.from ?? ["ACTIVE"];
39
+ if (!validFromStatuses.includes(user.status)) {
40
+ throw new InvalidStatusTransitionError(user.status, targetStatus);
41
+ }
42
+ ```
43
+
44
+ - Default `from` contains the base valid source status
45
+ - Parent modules can override to allow transitions from additional statuses
@@ -0,0 +1,7 @@
1
+ # Error Classes
2
+
3
+ Naming convention:
4
+
5
+ - `name`: Class name exactly, `as const`
6
+ - `code`: SCREAMING_SNAKE_CASE, `as const`
7
+ - Constructor includes context (IDs, values)
@@ -0,0 +1,8 @@
1
+ # Module Exports
2
+
3
+ Export order:
4
+
5
+ 1. `defineModule` from module.ts
6
+ 2. Generated types (enum values + types separately)
7
+ 3. Error classes (for typed catch blocks)
8
+ 4. Domain functions with input types
@@ -0,0 +1,30 @@
1
+ # Database Models
2
+
3
+ ## Factory Function Pattern
4
+
5
+ ```typescript
6
+ export function createEntityType(params: {
7
+ fields?: Record<string, unknown>;
8
+ additionalStatuses?: string[];
9
+ });
10
+ ```
11
+
12
+ - Include `...db.fields.timestamps()`
13
+ - Use `.description()` for field docs
14
+ - Apply permissions at model level
15
+
16
+ ## Stateful Model Enums
17
+
18
+ Status enums are tied to the module's state machine. Base module defines core statuses; parent callers can only **extend, not replace**.
19
+
20
+ ```typescript
21
+ // Good: extension only
22
+ const BASE_STATUSES = ["PENDING", "ACTIVE", "INACTIVE"] as const;
23
+ const statuses = [...BASE_STATUSES, ...(params.additionalStatuses ?? [])];
24
+
25
+ // Bad: allows replacement
26
+ const statuses = params.statuses ?? ["PENDING", "ACTIVE", "INACTIVE"];
27
+ ```
28
+
29
+ - Name parameter `additionalX` to signal extension-only
30
+ - Parent modules handle their additional status transitions
@@ -0,0 +1,29 @@
1
+ # Testing Patterns
2
+
3
+ ## Test Coverage Goal
4
+
5
+ Tests should cover all paths in the corresponding `docs/commands/*.md`:
6
+
7
+ - **Process Flow**: Each branch in the mermaid flowchart = one test case
8
+ - **Error Scenarios**: Each error code listed = one test case
9
+ - **Idempotent paths**: If flowchart shows "Already exists? → Return existing"
10
+
11
+ ## Mock Database
12
+
13
+ ```typescript
14
+ const { db, spies } = createMockDb<DB>();
15
+
16
+ // Single return
17
+ spies.select.mockReturnValue(entity);
18
+
19
+ // Sequential returns (in query execution order)
20
+ spies.select.mockReturnValueOnce(first).mockReturnValueOnce(second);
21
+ ```
22
+
23
+ ## Fixtures (`src/testing/fixtures.ts`)
24
+
25
+ - Import `Schema` from `lib/types` (not `Namespace` from generated code)
26
+ - Pattern: `export const baseEntity = { ... } as const satisfies Entity<Schema>`
27
+ - Fixed IDs for traceability: `"entity-1"`
28
+ - Consistent timestamp: `new Date("2024-01-01T00:00:00.000Z")`
29
+ - `updatedAt: null` for base fixtures
@@ -83,3 +83,7 @@ pnpm run app:doc:check
83
83
  ## Next Step
84
84
 
85
85
  After completing Tier 1-2, use `/app-compose-2-requirements-breakdown` to create Tier 3 documentation.
86
+
87
+ ## References
88
+
89
+ - [Application structure](references/structure.md)
@@ -1,8 +1,3 @@
1
- ---
2
- paths:
3
- - "examples/**/src/"
4
- ---
5
-
6
1
  # Application Directory Structure
7
2
 
8
3
  ```
@@ -86,3 +86,10 @@ pnpm run app:doc:check
86
86
  ## Next Step
87
87
 
88
88
  After completing Tier 3, use `/app-compose-3-doc-review` to validate documentation parity.
89
+
90
+ ## References
91
+
92
+ - [Application structure](references/structure.md)
93
+ - [ListView screen](references/screen-listview.md)
94
+ - [Form screen](references/screen-form.md)
95
+ - [DetailView screen](references/screen-detailview.md)
@@ -1,9 +1,3 @@
1
- ---
2
- paths:
3
- - "examples/**/src/pages/**/[id]/page.tsx"
4
- - "examples/**/src/pages/**/[id]/components/*.tsx"
5
- ---
6
-
7
1
  # DetailView Screen Implementation
8
2
 
9
3
  Implementation pattern for screens with `Screen Type: DetailView`.
@@ -1,9 +1,3 @@
1
- ---
2
- paths:
3
- - "examples/**/src/pages/**/page.tsx"
4
- - "examples/**/src/pages/**/components/*form*.tsx"
5
- ---
6
-
7
1
  # Form Screen Implementation
8
2
 
9
3
  Implementation pattern for screens with `Screen Type: Form`.
@@ -1,9 +1,3 @@
1
- ---
2
- paths:
3
- - "examples/**/src/pages/**/page.tsx"
4
- - "examples/**/src/pages/**/components/*table*.tsx"
5
- ---
6
-
7
1
  # ListView Screen Implementation
8
2
 
9
3
  Implementation pattern for screens with `Screen Type: ListView`.
@@ -0,0 +1,27 @@
1
+ # Application Directory Structure
2
+
3
+ ```
4
+ {app_name}/
5
+ ├── backend/
6
+ │ ├── src/
7
+ │ │ ├── modules.ts # Declaring module usage
8
+ │ │ ├── modules/
9
+ │ │ │ └── {module-name}/ # Module-specific directory
10
+ │ │ │ ├── resolvers/ # API Definition to expose graphql apis
11
+ │ │ │ └── executors/ # PubSub Automation (one file per declaration)
12
+ │ │ └── generated/ # Auto-generated code (do not edit)
13
+ │ └── tailor.config.ts # tailor application config
14
+
15
+ └── frontend/
16
+ └── src/
17
+ ├── pages/ # File-based routing (auto-discovered by Vite plugin)
18
+ │ └── {page-path}/
19
+ │ ├── page.tsx
20
+ │ └── {page-path}/
21
+ │ ├── components/
22
+ │ └── page.tsx
23
+ ├── components/
24
+ │ └── ui/ # Generic UI components
25
+ ├── graphql/ # gql.tada settings
26
+ └── providers/ # react providers
27
+ ```
@@ -110,3 +110,7 @@ examples/<app-name>/docs/actors/*.md # Actors
110
110
  ## Next Step
111
111
 
112
112
  After fixing all issues, use `/app-compose-4-design-mock` to create visual UI mockups from screen specifications.
113
+
114
+ ## References
115
+
116
+ - [Application structure](references/structure.md)
@@ -0,0 +1,27 @@
1
+ # Application Directory Structure
2
+
3
+ ```
4
+ {app_name}/
5
+ ├── backend/
6
+ │ ├── src/
7
+ │ │ ├── modules.ts # Declaring module usage
8
+ │ │ ├── modules/
9
+ │ │ │ └── {module-name}/ # Module-specific directory
10
+ │ │ │ ├── resolvers/ # API Definition to expose graphql apis
11
+ │ │ │ └── executors/ # PubSub Automation (one file per declaration)
12
+ │ │ └── generated/ # Auto-generated code (do not edit)
13
+ │ └── tailor.config.ts # tailor application config
14
+
15
+ └── frontend/
16
+ └── src/
17
+ ├── pages/ # File-based routing (auto-discovered by Vite plugin)
18
+ │ └── {page-path}/
19
+ │ ├── page.tsx
20
+ │ └── {page-path}/
21
+ │ ├── components/
22
+ │ └── page.tsx
23
+ ├── components/
24
+ │ └── ui/ # Generic UI components
25
+ ├── graphql/ # gql.tada settings
26
+ └── providers/ # react providers
27
+ ```
@@ -246,3 +246,11 @@ mcp__pencil__get_screenshot(filePath: "docs/design.pen", nodeId: "{frame-id}")
246
246
  ## Next Step
247
247
 
248
248
  After completing design mockups, use `/app-compose-5-design-mock-review` to review and validate the mockups against screen specifications.
249
+
250
+ ## References
251
+
252
+ - [Application structure](references/structure.md)
253
+ - [ListView screen](references/screen-listview.md)
254
+ - [Form screen](references/screen-form.md)
255
+ - [DetailView screen](references/screen-detailview.md)
256
+ - [Page components](references/component.md)
@@ -1,8 +1,3 @@
1
- ---
2
- paths:
3
- - "examples/**/src/pages/**/components/*.tsx"
4
- ---
5
-
6
1
  # Page Components
7
2
 
8
3
  Page-specific components are placed in a `components/` directory, separated from page.tsx.
@@ -0,0 +1,106 @@
1
+ # DetailView Screen Implementation
2
+
3
+ Implementation pattern for screens with `Screen Type: DetailView`.
4
+ Assumes `page.md` and `component.md` rules.
5
+
6
+ ## File Structure
7
+
8
+ ```
9
+ {screen-path}/[id]/
10
+ ├── components/
11
+ │ ├── {screen-name}-detail.tsx # Main content (left column)
12
+ │ └── {screen-name}-actions.tsx # Action sidebar (right column)
13
+ ├── edit/
14
+ │ ├── components/
15
+ │ │ └── edit-{screen-name}-form.tsx
16
+ │ └── page.tsx
17
+ └── page.tsx
18
+ ```
19
+
20
+ ## Layout
21
+
22
+ - Two-column layout: main content on the left, actions on the right.
23
+
24
+ ```tsx
25
+ const ResourcePage = () => {
26
+ const { id } = useParams();
27
+ const [{ data, error, fetching }, reexecuteQuery] = useQuery({
28
+ query: ResourceQuery,
29
+ variables: { id: id! },
30
+ });
31
+
32
+ if (fetching) return <Loading />;
33
+ if (error || !data?.resource) return <ErrorFallback ... />;
34
+
35
+ return (
36
+ <Layout columns={2} title="Resource Detail">
37
+ <Layout.Column>
38
+ <ResourceDetail resource={data.resource} />
39
+ </Layout.Column>
40
+ <Layout.Column>
41
+ <ResourceActions resource={data.resource} />
42
+ </Layout.Column>
43
+ </Layout>
44
+ );
45
+ };
46
+ ```
47
+
48
+ ## Left Column: Detail Component
49
+
50
+ Stack `DescriptionCard` and related tables vertically with `space-y-6`.
51
+
52
+ - `DescriptionCard` (`@tailor-platform/app-shell`): renders key-value fields declaratively.
53
+ - Complex content (tables, timelines): wrap in `<div className="rounded-lg border bg-card p-6">`.
54
+
55
+ ### DescriptionCard
56
+
57
+ ```tsx
58
+ <DescriptionCard
59
+ data={resource}
60
+ title="Overview"
61
+ columns={3}
62
+ fields={[
63
+ { key: "name", label: "Name", meta: { copyable: true } },
64
+ {
65
+ key: "status",
66
+ label: "Status",
67
+ type: "badge",
68
+ meta: { badgeVariantMap: { ACTIVE: "success", PENDING: "warning" } },
69
+ },
70
+ { type: "divider" },
71
+ {
72
+ key: "createdAt",
73
+ label: "Created At",
74
+ type: "date",
75
+ meta: { dateFormat: "medium" },
76
+ },
77
+ ]}
78
+ />
79
+ ```
80
+
81
+ Field types: `"text"` (default), `"badge"`, `"money"`, `"date"`, `"link"`, `"address"`, `"reference"`, `"divider"`
82
+
83
+ ## Right Column: Actions Component
84
+
85
+ Wrap in a `Card` component. Use `Button variant="ghost"` for each action item.
86
+
87
+ ```tsx
88
+ <Card>
89
+ <CardHeader>
90
+ <CardTitle>Actions</CardTitle>
91
+ </CardHeader>
92
+ <CardContent className="space-y-2">
93
+ <Button variant="ghost" className="w-full justify-start gap-2" asChild>
94
+ <Link to="edit">✎ Edit</Link>
95
+ </Button>
96
+ <Button variant="ghost" className="w-full justify-start gap-2" onClick={handler}>
97
+ ✓ Approve
98
+ </Button>
99
+ </CardContent>
100
+ </Card>
101
+ ```
102
+
103
+ - Navigation: `<Button variant="ghost" asChild><Link to="...">`
104
+ - Mutation: `<Button variant="ghost" onClick={handler}>` with custom resolvers (see `backend/resolvers.md`)
105
+ - Conditional: show/hide based on status
106
+ - Multiple cards: stack with `<div className="space-y-6">`
@@ -0,0 +1,139 @@
1
+ # Form Screen Implementation
2
+
3
+ Implementation pattern for screens with `Screen Type: Form`.
4
+ Assumes `page.md` and `component.md` rules.
5
+
6
+ ## File Structure
7
+
8
+ ```
9
+ {screen-path}/
10
+ ├── components/
11
+ │ └── {screen-name}-form.tsx # Form component with validation
12
+ └── page.tsx
13
+ ```
14
+
15
+ ## Page Component (page.tsx)
16
+
17
+ Form pages delegate mutation logic to the form component.
18
+
19
+ ```tsx
20
+ const ScreenNamePage = () => (
21
+ <Layout columns={1} title="Screen Title">
22
+ <Layout.Column>
23
+ <ScreenNameForm />
24
+ </Layout.Column>
25
+ </Layout>
26
+ );
27
+ ```
28
+
29
+ For edit forms that need existing data, co-locate data fetching in the page component:
30
+
31
+ ```tsx
32
+ const EditPage = () => {
33
+ const { id } = useParams();
34
+ const [{ data, error, fetching }] = useQuery({
35
+ query: ResourceQuery,
36
+ variables: { id: id! },
37
+ });
38
+
39
+ if (fetching) return <Loading />;
40
+ if (error || !data?.resource) return <ErrorFallback ... />;
41
+
42
+ return (
43
+ <Layout columns={1} title="Edit Resource">
44
+ <Layout.Column>
45
+ <EditResourceForm resource={data.resource} />
46
+ </Layout.Column>
47
+ </Layout>
48
+ );
49
+ };
50
+ ```
51
+
52
+ ## Form Component (components/{screen-name}-form.tsx)
53
+
54
+ ### Technology Stack
55
+
56
+ - `react-hook-form` — form state management
57
+ - `zod` + `@hookform/resolvers/zod` — validation
58
+ - `useMutation` (urql) — GraphQL mutation
59
+ - `useNavigate` (@tailor-platform/app-shell) — post-submit navigation
60
+
61
+ ### Pattern
62
+
63
+ ```tsx
64
+ const formSchema = z.object({
65
+ title: z.string().min(1, "Title is required"),
66
+ description: z.string().optional(),
67
+ });
68
+
69
+ type FormValues = z.infer<typeof formSchema>;
70
+
71
+ export const ScreenNameForm = () => {
72
+ const navigate = useNavigate();
73
+ const [, createResource] = useMutation(CreateMutation);
74
+
75
+ const form = useForm<FormValues>({
76
+ resolver: zodResolver(formSchema),
77
+ defaultValues: { title: "", description: "" },
78
+ });
79
+
80
+ const onSubmit = (values: FormValues) => {
81
+ void createResource({ input: values }).then((result) => {
82
+ if (!result.error) {
83
+ void navigate("..");
84
+ }
85
+ });
86
+ };
87
+
88
+ return (
89
+ <Form {...form}>
90
+ <form onSubmit={(e) => void form.handleSubmit(onSubmit)(e)} className="max-w-md space-y-4">
91
+ <FormField
92
+ control={form.control}
93
+ name="title"
94
+ render={({ field }) => (
95
+ <FormItem>
96
+ <FormLabel>Title</FormLabel>
97
+ <FormControl>
98
+ <Input placeholder="Enter title" {...field} />
99
+ </FormControl>
100
+ <FormMessage />
101
+ </FormItem>
102
+ )}
103
+ />
104
+ <div className="flex gap-2">
105
+ <Button type="submit">Create</Button>
106
+ <Button type="button" variant="outline" onClick={() => void navigate("..")}>
107
+ Cancel
108
+ </Button>
109
+ </div>
110
+ </form>
111
+ </Form>
112
+ );
113
+ };
114
+ ```
115
+
116
+ ## Field Type Mapping
117
+
118
+ | Field Type | Component | Zod Schema |
119
+ | ---------- | ------------------------------ | ------------------------------- |
120
+ | Text | `<Input />` | `z.string()` |
121
+ | Textarea | `<textarea className="..." />` | `z.string()` |
122
+ | Dropdown | `<Select />` | `z.string()` or `z.enum([...])` |
123
+ | Date | `<Input type="date" />` | `z.string()` (ISO format) |
124
+ | Number | `<Input type="number" />` | `z.coerce.number()` |
125
+ | Email | `<Input type="email" />` | `z.string().email()` |
126
+ | Checkbox | `<Checkbox />` | `z.boolean()` |
127
+ | Radio | `<RadioGroup />` | `z.enum([...])` |
128
+
129
+ ## Validation Mapping
130
+
131
+ - **Required: Yes** → `.min(1, "Field is required")` (string) / `.positive()` (number)
132
+ - **Required: No** → `.optional()`
133
+
134
+ ## Key Points
135
+
136
+ - Set `defaultValues` for all fields (empty string, false, etc.)
137
+ - Navigate to `".."` after successful mutation
138
+ - Cancel button must use `type="button"` to prevent form submit
139
+ - For edit forms, accept fragment data as props and pre-fill `defaultValues`
@@ -0,0 +1,153 @@
1
+ # ListView Screen Implementation
2
+
3
+ Implementation pattern for screens with `Screen Type: ListView`.
4
+ Assumes `page.md` and `component.md` rules.
5
+
6
+ ## File Structure
7
+
8
+ ```
9
+ {screen-path}/
10
+ ├── components/
11
+ │ └── {screen-name}-table.tsx # Table component with fragments
12
+ └── page.tsx
13
+ ```
14
+
15
+ ## Page Component (page.tsx)
16
+
17
+ Data fetching and `Layout` must be co-located in the same page component.
18
+ Do NOT split into an inner Content component — `Layout` requires `Layout.Column` as direct children.
19
+
20
+ ```tsx
21
+ const ResourcesQuery = graphql(
22
+ `
23
+ query Resources {
24
+ resources {
25
+ ...ResourceTable
26
+ }
27
+ }
28
+ `,
29
+ [ResourceTableFragment],
30
+ );
31
+
32
+ const ResourcesPage = () => {
33
+ const [{ data, error, fetching }, reexecuteQuery] = useQuery({
34
+ query: ResourcesQuery,
35
+ });
36
+
37
+ if (fetching) return <Loading />;
38
+
39
+ if (error || !data) {
40
+ return (
41
+ <ErrorFallback
42
+ title="Failed to load resources"
43
+ message="An error occurred while fetching the list."
44
+ onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
45
+ />
46
+ );
47
+ }
48
+
49
+ return (
50
+ <Layout
51
+ columns={1}
52
+ title="Resources"
53
+ actions={[
54
+ <Button key="create" asChild>
55
+ <Link to="create">Create</Link>
56
+ </Button>,
57
+ ]}
58
+ >
59
+ <Layout.Column>
60
+ <ResourceTable data={data.resources} />
61
+ </Layout.Column>
62
+ </Layout>
63
+ );
64
+ };
65
+ ```
66
+
67
+ ## Table Component (components/{screen-name}-table.tsx)
68
+
69
+ ### Fragment Collocation
70
+
71
+ Define a row fragment and a table fragment wrapping the Connection type.
72
+
73
+ ```tsx
74
+ const ResourceRowFragment = graphql(`
75
+ fragment ResourceRow on Resource {
76
+ id
77
+ title
78
+ status
79
+ createdAt
80
+ }
81
+ `);
82
+
83
+ export const ResourceTableFragment = graphql(
84
+ `
85
+ fragment ResourceTable on ResourceConnection {
86
+ edges {
87
+ node {
88
+ ...ResourceRow
89
+ }
90
+ }
91
+ }
92
+ `,
93
+ [ResourceRowFragment],
94
+ );
95
+ ```
96
+
97
+ ### Row Component
98
+
99
+ ```tsx
100
+ const ResourceRow = ({ resource: resourceFragment }: ResourceRowProps) => {
101
+ const resource = readFragment(ResourceRowFragment, resourceFragment);
102
+ return (
103
+ <TableRow>
104
+ <TableCell>{resource.title}</TableCell>
105
+ <TableCell>
106
+ <Badge variant={resource.status === "ACTIVE" ? "default" : "secondary"}>
107
+ {resource.status}
108
+ </Badge>
109
+ </TableCell>
110
+ <TableCell>
111
+ <Button variant="ghost" size="sm" asChild>
112
+ <Link to={resource.id}>View</Link>
113
+ </Button>
114
+ </TableCell>
115
+ </TableRow>
116
+ );
117
+ };
118
+ ```
119
+
120
+ ### Empty State
121
+
122
+ ```tsx
123
+ if (connection.edges.length === 0) {
124
+ return (
125
+ <EmptyState
126
+ title="No resources"
127
+ message="Get started by creating a new resource."
128
+ action={
129
+ <Button asChild>
130
+ <Link to="create">Create</Link>
131
+ </Button>
132
+ }
133
+ />
134
+ );
135
+ }
136
+ ```
137
+
138
+ ## Column Property Mapping
139
+
140
+ | Property | Implementation |
141
+ | --------------- | ----------------------------------------------------------- |
142
+ | Hideable: Yes | Column visibility state to toggle show/hide |
143
+ | Sortable: Yes | Sort icon on `<TableHead>` + onClick to toggle query vars |
144
+ | Filterable: Yes | Filter UI above table (`<Select />`) |
145
+ | Searchable: Yes | Search input above table (`<Input placeholder="Search" />`) |
146
+
147
+ ## Key Points
148
+
149
+ - Use `<Badge />` for status columns
150
+ - Format dates with `toLocaleDateString()`
151
+ - Use `<EmptyState />` with Create action for empty lists
152
+ - Add a View button per row to navigate to the detail page
153
+ - Iterate data using Connection type `edges.node` pattern