@tailor-platform/erp-kit 0.0.1 → 0.1.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 (213) hide show
  1. package/README.md +196 -28
  2. package/dist/cli.js +894 -0
  3. package/package.json +65 -8
  4. package/rules/app-compose/backend/auth.md +78 -0
  5. package/rules/app-compose/frontend/auth.md +55 -0
  6. package/rules/app-compose/frontend/component.md +55 -0
  7. package/rules/app-compose/frontend/page.md +86 -0
  8. package/rules/app-compose/frontend/screen-detailview.md +112 -0
  9. package/rules/app-compose/frontend/screen-form.md +145 -0
  10. package/rules/app-compose/frontend/screen-listview.md +159 -0
  11. package/rules/app-compose/structure.md +32 -0
  12. package/rules/module-development/commands.md +54 -0
  13. package/rules/module-development/cross-module-type-injection.md +28 -0
  14. package/rules/module-development/dependency-modules.md +24 -0
  15. package/rules/module-development/errors.md +12 -0
  16. package/rules/module-development/executors.md +67 -0
  17. package/rules/module-development/exports.md +13 -0
  18. package/rules/module-development/models.md +34 -0
  19. package/rules/module-development/structure.md +27 -0
  20. package/rules/module-development/sync-vs-async-operations.md +83 -0
  21. package/rules/module-development/testing.md +43 -0
  22. package/rules/sdk-best-practices/db-relations.md +74 -0
  23. package/rules/sdk-best-practices/sdk-docs.md +14 -0
  24. package/schemas/app-compose/actors.yml +34 -0
  25. package/schemas/app-compose/business-flow.yml +50 -0
  26. package/schemas/app-compose/requirements.yml +33 -0
  27. package/schemas/app-compose/resolver.yml +47 -0
  28. package/schemas/app-compose/screen.yml +81 -0
  29. package/schemas/app-compose/story.yml +67 -0
  30. package/schemas/module/command.yml +52 -0
  31. package/schemas/module/feature.yml +58 -0
  32. package/schemas/module/model.yml +70 -0
  33. package/schemas/module/module.yml +50 -0
  34. package/skills/1-module-docs/SKILL.md +107 -0
  35. package/skills/2-module-feature-breakdown/SKILL.md +66 -0
  36. package/skills/3-module-doc-review/SKILL.md +230 -0
  37. package/skills/4-module-tdd-implementation/SKILL.md +56 -0
  38. package/skills/5-module-implementation-review/SKILL.md +400 -0
  39. package/skills/app-compose-1-requirement-analysis/SKILL.md +85 -0
  40. package/skills/app-compose-2-requirements-breakdown/SKILL.md +88 -0
  41. package/skills/app-compose-3-doc-review/SKILL.md +112 -0
  42. package/skills/app-compose-4-design-mock/SKILL.md +248 -0
  43. package/skills/app-compose-5-design-mock-review/SKILL.md +283 -0
  44. package/skills/app-compose-6-implementation-spec/SKILL.md +122 -0
  45. package/skills/mock-scenario/SKILL.md +118 -0
  46. package/src/app.ts +1 -0
  47. package/src/cli.ts +120 -0
  48. package/src/commands/check.test.ts +30 -0
  49. package/src/commands/check.ts +66 -0
  50. package/src/commands/init.test.ts +77 -0
  51. package/src/commands/init.ts +87 -0
  52. package/src/commands/mock/index.ts +53 -0
  53. package/src/commands/mock/start.ts +179 -0
  54. package/src/commands/mock/validate.test.ts +185 -0
  55. package/src/commands/mock/validate.ts +198 -0
  56. package/src/commands/scaffold.test.ts +76 -0
  57. package/src/commands/scaffold.ts +119 -0
  58. package/src/commands/sync-check.test.ts +125 -0
  59. package/src/commands/sync-check.ts +182 -0
  60. package/src/integration.test.ts +63 -0
  61. package/src/mdschema.ts +48 -0
  62. package/src/mockServer.ts +55 -0
  63. package/src/module.ts +86 -0
  64. package/src/modules/accounting/.gitkeep +0 -0
  65. package/src/modules/coa-management/.gitkeep +0 -0
  66. package/src/modules/inventory/.gitkeep +0 -0
  67. package/src/modules/manufacturing/.gitkeep +0 -0
  68. package/src/modules/primitives/README.md +39 -0
  69. package/src/modules/primitives/command/activateCategory.test.ts +75 -0
  70. package/src/modules/primitives/command/activateCategory.ts +50 -0
  71. package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
  72. package/src/modules/primitives/command/activateCurrency.ts +50 -0
  73. package/src/modules/primitives/command/activateUnit.test.ts +53 -0
  74. package/src/modules/primitives/command/activateUnit.ts +50 -0
  75. package/src/modules/primitives/command/convertAmount.test.ts +275 -0
  76. package/src/modules/primitives/command/convertAmount.ts +126 -0
  77. package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
  78. package/src/modules/primitives/command/convertQuantity.ts +73 -0
  79. package/src/modules/primitives/command/createCategory.test.ts +126 -0
  80. package/src/modules/primitives/command/createCategory.ts +89 -0
  81. package/src/modules/primitives/command/createCurrency.test.ts +191 -0
  82. package/src/modules/primitives/command/createCurrency.ts +77 -0
  83. package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
  84. package/src/modules/primitives/command/createExchangeRate.ts +91 -0
  85. package/src/modules/primitives/command/createUnit.test.ts +214 -0
  86. package/src/modules/primitives/command/createUnit.ts +88 -0
  87. package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
  88. package/src/modules/primitives/command/deactivateCategory.ts +62 -0
  89. package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
  90. package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
  91. package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
  92. package/src/modules/primitives/command/deactivateUnit.ts +62 -0
  93. package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
  94. package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
  95. package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
  96. package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
  97. package/src/modules/primitives/db/currency.ts +30 -0
  98. package/src/modules/primitives/db/exchangeRate.ts +28 -0
  99. package/src/modules/primitives/db/unit.ts +32 -0
  100. package/src/modules/primitives/db/uomCategory.ts +32 -0
  101. package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
  102. package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
  103. package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
  104. package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
  105. package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
  106. package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
  107. package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
  108. package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
  109. package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
  110. package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
  111. package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
  112. package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
  113. package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
  114. package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
  115. package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
  116. package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
  117. package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
  118. package/src/modules/primitives/docs/features/uom-categories.md +52 -0
  119. package/src/modules/primitives/docs/models/Currency.md +45 -0
  120. package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
  121. package/src/modules/primitives/docs/models/Unit.md +46 -0
  122. package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
  123. package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
  124. package/src/modules/primitives/index.ts +40 -0
  125. package/src/modules/primitives/lib/errors.ts +138 -0
  126. package/src/modules/primitives/lib/types.ts +20 -0
  127. package/src/modules/primitives/module.ts +66 -0
  128. package/src/modules/primitives/permissions.ts +18 -0
  129. package/src/modules/primitives/tailor.config.ts +11 -0
  130. package/src/modules/primitives/testing/fixtures.ts +161 -0
  131. package/src/modules/product-management/.gitkeep +0 -0
  132. package/src/modules/purchase/.gitkeep +0 -0
  133. package/src/modules/sales/.gitkeep +0 -0
  134. package/src/modules/shared/createContext.test.ts +39 -0
  135. package/src/modules/shared/createContext.ts +15 -0
  136. package/src/modules/shared/defineCommand.test.ts +42 -0
  137. package/src/modules/shared/defineCommand.ts +19 -0
  138. package/src/modules/shared/definePermissions.test.ts +146 -0
  139. package/src/modules/shared/definePermissions.ts +94 -0
  140. package/src/modules/shared/entityTypes.ts +15 -0
  141. package/src/modules/shared/errors.ts +22 -0
  142. package/src/modules/shared/index.ts +1 -0
  143. package/src/modules/shared/internal.ts +13 -0
  144. package/src/modules/shared/requirePermission.test.ts +47 -0
  145. package/src/modules/shared/requirePermission.ts +8 -0
  146. package/src/modules/shared/types.ts +4 -0
  147. package/src/modules/supplier-management/.gitkeep +0 -0
  148. package/src/modules/supplier-portal/.gitkeep +0 -0
  149. package/src/modules/testing/index.ts +120 -0
  150. package/src/modules/user-management/README.md +38 -0
  151. package/src/modules/user-management/command/activateUser.test.ts +112 -0
  152. package/src/modules/user-management/command/activateUser.ts +67 -0
  153. package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
  154. package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
  155. package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
  156. package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
  157. package/src/modules/user-management/command/createPermission.test.ts +143 -0
  158. package/src/modules/user-management/command/createPermission.ts +66 -0
  159. package/src/modules/user-management/command/createRole.test.ts +115 -0
  160. package/src/modules/user-management/command/createRole.ts +52 -0
  161. package/src/modules/user-management/command/createUser.test.ts +198 -0
  162. package/src/modules/user-management/command/createUser.ts +85 -0
  163. package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
  164. package/src/modules/user-management/command/deactivateUser.ts +67 -0
  165. package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
  166. package/src/modules/user-management/command/logAuditEvent.ts +59 -0
  167. package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
  168. package/src/modules/user-management/command/reactivateUser.ts +67 -0
  169. package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
  170. package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
  171. package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
  172. package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
  173. package/src/modules/user-management/db/auditEvent.ts +47 -0
  174. package/src/modules/user-management/db/permission.ts +31 -0
  175. package/src/modules/user-management/db/role.ts +28 -0
  176. package/src/modules/user-management/db/rolePermission.ts +44 -0
  177. package/src/modules/user-management/db/user.ts +38 -0
  178. package/src/modules/user-management/db/userRole.ts +44 -0
  179. package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
  180. package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
  181. package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
  182. package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
  183. package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
  184. package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
  185. package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
  186. package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
  187. package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
  188. package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
  189. package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
  190. package/src/modules/user-management/docs/features/audit-trail.md +80 -0
  191. package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
  192. package/src/modules/user-management/docs/features/user-account-management.md +64 -0
  193. package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
  194. package/src/modules/user-management/docs/models/Permission.md +31 -0
  195. package/src/modules/user-management/docs/models/Role.md +31 -0
  196. package/src/modules/user-management/docs/models/RolePermission.md +33 -0
  197. package/src/modules/user-management/docs/models/User.md +47 -0
  198. package/src/modules/user-management/docs/models/UserRole.md +34 -0
  199. package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
  200. package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
  201. package/src/modules/user-management/generated/enums.ts +24 -0
  202. package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
  203. package/src/modules/user-management/index.ts +32 -0
  204. package/src/modules/user-management/lib/errors.ts +81 -0
  205. package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
  206. package/src/modules/user-management/lib/types.ts +31 -0
  207. package/src/modules/user-management/module.ts +77 -0
  208. package/src/modules/user-management/permissions.ts +15 -0
  209. package/src/modules/user-management/tailor.config.ts +11 -0
  210. package/src/modules/user-management/testing/fixtures.ts +98 -0
  211. package/src/schemas.ts +25 -0
  212. package/src/testing.ts +10 -0
  213. package/src/util.ts +3 -0
package/package.json CHANGED
@@ -1,10 +1,67 @@
1
1
  {
2
2
  "name": "@tailor-platform/erp-kit",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @tailor-platform/erp-kit",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
3
+ "version": "0.1.0",
4
+ "description": "Opinionated ERP toolkit for building business applications on Tailor Platform",
5
+ "bin": {
6
+ "erp-kit": "./dist/cli.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "rules",
11
+ "schemas",
12
+ "skills",
13
+ "src"
14
+ ],
15
+ "type": "module",
16
+ "exports": {
17
+ "./module": "./src/module.ts",
18
+ "./app": "./src/app.ts",
19
+ "./testing": "./src/testing.ts"
20
+ },
21
+ "dependencies": {
22
+ "@jackchuka/mdschema": "^0.12.3",
23
+ "@mockoon/commons": "^9.0.0",
24
+ "@mockoon/commons-server": "^9.0.0",
25
+ "chalk": "^5.4.1",
26
+ "fast-glob": "^3.3.3",
27
+ "politty": "^0.4.0",
28
+ "zod": "4.3.6"
29
+ },
30
+ "devDependencies": {
31
+ "@tailor-platform/function-kysely-tailordb": "0.1.3",
32
+ "@tailor-platform/function-types": "0.8.1",
33
+ "@tailor-platform/sdk": "1.17.1",
34
+ "@types/node": "^25.1.0",
35
+ "eslint": "9.39.2",
36
+ "kysely": "0.28.10",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.7.3",
39
+ "vitest": "^4.0.0",
40
+ "@tailor-platform/eslint-config": "0.0.1"
41
+ },
42
+ "peerDependencies": {
43
+ "@tailor-platform/function-kysely-tailordb": "0.1.3",
44
+ "@tailor-platform/function-types": "0.8.1",
45
+ "@tailor-platform/sdk": "1.17.1",
46
+ "kysely": "0.28.10"
47
+ },
48
+ "engines": {
49
+ "node": ">=24"
50
+ },
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "generate": "pnpm generate:primitives && pnpm generate:user-management",
54
+ "generate:primitives": "cd src/modules/primitives && tailor-sdk generate",
55
+ "generate:user-management": "cd src/modules/user-management && tailor-sdk generate",
56
+ "lint": "eslint --cache .",
57
+ "lint:fix": "eslint --cache --fix .",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "test:primitives": "vitest run src/modules/primitives/",
62
+ "test:user-management": "vitest run src/modules/user-management/",
63
+ "test:shared": "vitest run src/modules/shared/",
64
+ "mock:start": "node dist/cli.js mock start",
65
+ "mock:validate": "node dist/cli.js mock validate"
66
+ }
67
+ }
@@ -0,0 +1,78 @@
1
+ ---
2
+ paths:
3
+ - "examples/**/backend/tailor.config.ts"
4
+ - "examples/**/backend/.env"
5
+ ---
6
+
7
+ # Backend Auth Configuration
8
+
9
+ Use Tailor SDK auth resources as the default pattern for backend authentication setup.
10
+
11
+ ## Standard Auth Shape
12
+
13
+ In `tailor.config.ts`:
14
+
15
+ - Define IdP with `defineIdp(...)`
16
+ - Define Auth with `defineAuth(...)`
17
+ - Define static website with `defineStaticWebSite(...)` when frontend is deployed as static hosting
18
+ - Wire `idProvider` via `idp.provider(...)`
19
+ - Set exactly one OAuth2 client for frontend login (default client name: `default`)
20
+ - Export config with `auth: auth` and `idp: [idp]`
21
+ - Include static website URL in CORS when using deployed frontend (for example, `cors: ["http://localhost:5173", website.url]`)
22
+
23
+ Prefer this stable naming unless there is a clear reason to change:
24
+
25
+ - Auth name: `default`
26
+ - IdP name: `default`
27
+ - OAuth2 client name: `default`
28
+
29
+ ## User Mapping
30
+
31
+ Use `userProfile` to map authenticated identities to the application user type.
32
+
33
+ - `type`: module-provided user type
34
+ - `usernameField`: stable unique field (for example, `email`)
35
+ - `attributes`: explicitly list required attributes
36
+
37
+ Avoid implicit attribute assumptions.
38
+
39
+ ## CORS for Static Website Login
40
+
41
+ When frontend is deployed via `staticWebsites`, OAuth metadata and query calls are cross-origin from `https://<name>-<workspace>.web.erp.dev` to app backend.
42
+
43
+ - Always include the deployed static website origin via `website.url` in backend `cors`
44
+ - Keep local development origin in `cors` as well (for example, `http://localhost:5173`)
45
+ - If your template uses IP allow lists, set `allowedIpAddresses` appropriately for Tailor internal access
46
+
47
+ If `website.url` is missing from `cors`, login may fail with browser CORS errors before authentication starts.
48
+
49
+ ## Seed Permission Alignment
50
+
51
+ Seeded login users in `seed/data/User.jsonl` must receive module permissions required by guarded commands/resolvers.
52
+
53
+ - Do not assume `ADMIN` is automatically privileged unless your permission evaluator explicitly supports it
54
+ - Grant concrete permission keys defined by the module (for example, `user-management:createUser`)
55
+ - Keep `User.jsonl` permissions aligned with the module currently wired in `src/modules.ts`
56
+
57
+ ## Workspace and CLI Inputs
58
+
59
+ Use `TAILOR_PLATFORM_WORKSPACE_ID` in backend `.env` for CLI operations.
60
+
61
+ Required for non-interactive resource lookup and automation:
62
+
63
+ - `tailor-sdk oauth2client list`
64
+ - `tailor-sdk oauth2client get <name>`
65
+
66
+ Use `--env-file <backend/.env>` for deterministic command execution in local/dev scripts.
67
+
68
+ ## OAuth Client ID Source of Truth
69
+
70
+ Frontend `VITE_TAILOR_CLIENT_ID` must come from backend-managed OAuth2 client data.
71
+
72
+ Canonical retrieval flow:
73
+
74
+ 1. `tailor-sdk oauth2client get default --json`
75
+ 2. Copy `clientId` from command output
76
+ 3. Set it in frontend env
77
+
78
+ Do not hardcode OAuth client IDs in source code.
@@ -0,0 +1,55 @@
1
+ ---
2
+ paths:
3
+ - "examples/**/frontend/src/App.tsx"
4
+ - "examples/**/frontend/src/lib/auth-client.ts"
5
+ - "examples/**/frontend/src/providers/graphql-provider.tsx"
6
+ ---
7
+
8
+ # Frontend Auth (AppShell AuthProvider)
9
+
10
+ Use `@tailor-platform/app-shell` authentication as the default pattern for frontend apps.
11
+
12
+ ## Required Environment Variables
13
+
14
+ - `VITE_TAILOR_APP_URL`
15
+ - `VITE_TAILOR_CLIENT_ID`
16
+
17
+ Fail fast at startup if either value is missing.
18
+
19
+ ## Auth Client Initialization
20
+
21
+ Create a single shared auth client in `src/lib/auth-client.ts`:
22
+
23
+ - Use `createAuthClient({ appUri, clientId })`
24
+ - Export it as `authClient`
25
+ - Do not create auth clients inside render functions
26
+
27
+ ## App Root Composition
28
+
29
+ Wrap the app with `AuthProvider` in `src/App.tsx`:
30
+
31
+ - `AuthProvider` must receive the shared `authClient`
32
+ - Use `guardComponent` for unauthenticated/loading states
33
+ - `guardComponent` should use `useAuth()` and handle:
34
+ - `!isReady`: loading UI
35
+ - `!isAuthenticated`: login UI with `login()`
36
+ - `error`: visible message
37
+
38
+ Recommended order:
39
+
40
+ 1. `AuthProvider`
41
+ 2. `GraphQLProvider`
42
+ 3. `AppShell`
43
+
44
+ ## GraphQL Authentication
45
+
46
+ `src/providers/graphql-provider.tsx` must attach OAuth headers for every request:
47
+
48
+ - Accept `authClient` as a prop
49
+ - Call `authClient.getAuthHeadersForQuery()`
50
+ - Set both headers:
51
+ - `Authorization`
52
+ - `DPoP`
53
+ - Keep `Content-Type: application/json`
54
+
55
+ Do not use unauthenticated static headers for `/query`.
@@ -0,0 +1,55 @@
1
+ ---
2
+ paths:
3
+ - "examples/**/src/pages/**/components/*.tsx"
4
+ ---
5
+
6
+ # Page Components
7
+
8
+ Page-specific components are placed in a `components/` directory, separated from page.tsx.
9
+
10
+ ```
11
+ {page-name}/
12
+ ├── components/
13
+ │ └── *.tsx
14
+ └── page.tsx
15
+ ```
16
+
17
+ ## Fragment Collocation
18
+
19
+ Components define and export their own GraphQL Fragment for the data they display. The parent page imports the Fragment and includes it in the query.
20
+
21
+ Use `graphql`, `FragmentOf`, and `readFragment` from `@/graphql`.
22
+
23
+ ```tsx
24
+ // components/user-card.tsx
25
+ import { graphql, type FragmentOf, readFragment } from "@/graphql";
26
+
27
+ export const UserCardFragment = graphql(`
28
+ fragment UserCard on User {
29
+ id
30
+ name
31
+ email
32
+ }
33
+ `);
34
+
35
+ export const UserCard = ({ user }: { user: FragmentOf<typeof UserCardFragment> }) => {
36
+ const data = readFragment(UserCardFragment, user);
37
+ return <div>{data.name}</div>;
38
+ };
39
+ ```
40
+
41
+ ```tsx
42
+ // page.tsx
43
+ import { UserCard, UserCardFragment } from "./components/user-card";
44
+
45
+ const UserQuery = graphql(
46
+ `
47
+ query User($id: ID!) {
48
+ user(id: $id) {
49
+ ...UserCard
50
+ }
51
+ }
52
+ `,
53
+ [UserCardFragment],
54
+ );
55
+ ```
@@ -0,0 +1,86 @@
1
+ ---
2
+ paths:
3
+ - "examples/**/src/pages/**/page.tsx"
4
+ ---
5
+
6
+ # Page File Structure (File-Based Routing)
7
+
8
+ Each `page.tsx` file should contain a default-exported page component with optional `appShellPageProps` static field.
9
+ Pages are automatically discovered by the Vite plugin.
10
+
11
+ ## Path Convention
12
+
13
+ The URL path is derived from the directory structure:
14
+
15
+ ```
16
+ src/pages/
17
+ ├── page.tsx # / (root path)
18
+ ├── purchasing/
19
+ │ ├── page.tsx # /purchasing
20
+ │ └── orders/
21
+ │ ├── page.tsx # /purchasing/orders
22
+ │ └── [id]/
23
+ │ └── page.tsx # /purchasing/orders/:id
24
+ └── (admin)/ # Grouping (not included in path)
25
+ └── settings/
26
+ └── page.tsx # /settings
27
+ ```
28
+
29
+ | Directory Name | Converts To | Description |
30
+ | -------------- | ----------- | ----------------------------- |
31
+ | `orders` | `orders` | Static segment |
32
+ | `[id]` | `:id` | Dynamic parameter |
33
+ | `(group)` | (excluded) | Grouping only (not in path) |
34
+ | `_lib` | (ignored) | Not routed (for shared logic) |
35
+
36
+ ## Page Component Pattern
37
+
38
+ ```tsx
39
+ import { Layout, type AppShellPageProps } from "@tailor-platform/app-shell";
40
+ import { useQuery } from "urql";
41
+ import { graphql } from "@/graphql";
42
+ import { ErrorFallback } from "@/components/composed/error-fallback";
43
+ import { Loading } from "@/components/composed/loading";
44
+
45
+ const MyQuery = graphql(`...`);
46
+
47
+ const MyPage = () => {
48
+ const [{ data, error, fetching }, reexecuteQuery] = useQuery({
49
+ query: MyQuery,
50
+ });
51
+
52
+ if (fetching) return <Loading />;
53
+
54
+ if (error || !data) {
55
+ return (
56
+ <ErrorFallback
57
+ title="Failed to load"
58
+ message="An error occurred while fetching data."
59
+ onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
60
+ />
61
+ );
62
+ }
63
+
64
+ return (
65
+ <Layout columns={1} title="My Page">
66
+ <Layout.Column>
67
+ <MyComponent data={data} />
68
+ </Layout.Column>
69
+ </Layout>
70
+ );
71
+ };
72
+
73
+ MyPage.appShellPageProps = {
74
+ meta: { title: "My Page" },
75
+ } satisfies AppShellPageProps;
76
+
77
+ export default MyPage;
78
+ ```
79
+
80
+ ## Key Points
81
+
82
+ - Handle `fetching` state with `<Loading />`
83
+ - Handle `error || !data` with `<ErrorFallback />` and `reexecuteQuery` for retry
84
+ - Use `appShellPageProps` static field for metadata (title, icon) and guards
85
+ - Guards on parent pages are automatically inherited by child pages
86
+ - See `component.md` for fragment collocation
@@ -0,0 +1,112 @@
1
+ ---
2
+ paths:
3
+ - "examples/**/src/pages/**/[id]/page.tsx"
4
+ - "examples/**/src/pages/**/[id]/components/*.tsx"
5
+ ---
6
+
7
+ # DetailView Screen Implementation
8
+
9
+ Implementation pattern for screens with `Screen Type: DetailView`.
10
+ Assumes `page.md` and `component.md` rules.
11
+
12
+ ## File Structure
13
+
14
+ ```
15
+ {screen-path}/[id]/
16
+ ├── components/
17
+ │ ├── {screen-name}-detail.tsx # Main content (left column)
18
+ │ └── {screen-name}-actions.tsx # Action sidebar (right column)
19
+ ├── edit/
20
+ │ ├── components/
21
+ │ │ └── edit-{screen-name}-form.tsx
22
+ │ └── page.tsx
23
+ └── page.tsx
24
+ ```
25
+
26
+ ## Layout
27
+
28
+ - Two-column layout: main content on the left, actions on the right.
29
+
30
+ ```tsx
31
+ const ResourcePage = () => {
32
+ const { id } = useParams();
33
+ const [{ data, error, fetching }, reexecuteQuery] = useQuery({
34
+ query: ResourceQuery,
35
+ variables: { id: id! },
36
+ });
37
+
38
+ if (fetching) return <Loading />;
39
+ if (error || !data?.resource) return <ErrorFallback ... />;
40
+
41
+ return (
42
+ <Layout columns={2} title="Resource Detail">
43
+ <Layout.Column>
44
+ <ResourceDetail resource={data.resource} />
45
+ </Layout.Column>
46
+ <Layout.Column>
47
+ <ResourceActions resource={data.resource} />
48
+ </Layout.Column>
49
+ </Layout>
50
+ );
51
+ };
52
+ ```
53
+
54
+ ## Left Column: Detail Component
55
+
56
+ Stack `DescriptionCard` and related tables vertically with `space-y-6`.
57
+
58
+ - `DescriptionCard` (`@tailor-platform/app-shell`): renders key-value fields declaratively.
59
+ - Complex content (tables, timelines): wrap in `<div className="rounded-lg border bg-card p-6">`.
60
+
61
+ ### DescriptionCard
62
+
63
+ ```tsx
64
+ <DescriptionCard
65
+ data={resource}
66
+ title="Overview"
67
+ columns={3}
68
+ fields={[
69
+ { key: "name", label: "Name", meta: { copyable: true } },
70
+ {
71
+ key: "status",
72
+ label: "Status",
73
+ type: "badge",
74
+ meta: { badgeVariantMap: { ACTIVE: "success", PENDING: "warning" } },
75
+ },
76
+ { type: "divider" },
77
+ {
78
+ key: "createdAt",
79
+ label: "Created At",
80
+ type: "date",
81
+ meta: { dateFormat: "medium" },
82
+ },
83
+ ]}
84
+ />
85
+ ```
86
+
87
+ Field types: `"text"` (default), `"badge"`, `"money"`, `"date"`, `"link"`, `"address"`, `"reference"`, `"divider"`
88
+
89
+ ## Right Column: Actions Component
90
+
91
+ Wrap in a `Card` component. Use `Button variant="ghost"` for each action item.
92
+
93
+ ```tsx
94
+ <Card>
95
+ <CardHeader>
96
+ <CardTitle>Actions</CardTitle>
97
+ </CardHeader>
98
+ <CardContent className="space-y-2">
99
+ <Button variant="ghost" className="w-full justify-start gap-2" asChild>
100
+ <Link to="edit">✎ Edit</Link>
101
+ </Button>
102
+ <Button variant="ghost" className="w-full justify-start gap-2" onClick={handler}>
103
+ ✓ Approve
104
+ </Button>
105
+ </CardContent>
106
+ </Card>
107
+ ```
108
+
109
+ - Navigation: `<Button variant="ghost" asChild><Link to="...">`
110
+ - Mutation: `<Button variant="ghost" onClick={handler}>` with custom resolvers (see `backend/resolvers.md`)
111
+ - Conditional: show/hide based on status
112
+ - Multiple cards: stack with `<div className="space-y-6">`
@@ -0,0 +1,145 @@
1
+ ---
2
+ paths:
3
+ - "examples/**/src/pages/**/page.tsx"
4
+ - "examples/**/src/pages/**/components/*form*.tsx"
5
+ ---
6
+
7
+ # Form Screen Implementation
8
+
9
+ Implementation pattern for screens with `Screen Type: Form`.
10
+ Assumes `page.md` and `component.md` rules.
11
+
12
+ ## File Structure
13
+
14
+ ```
15
+ {screen-path}/
16
+ ├── components/
17
+ │ └── {screen-name}-form.tsx # Form component with validation
18
+ └── page.tsx
19
+ ```
20
+
21
+ ## Page Component (page.tsx)
22
+
23
+ Form pages delegate mutation logic to the form component.
24
+
25
+ ```tsx
26
+ const ScreenNamePage = () => (
27
+ <Layout columns={1} title="Screen Title">
28
+ <Layout.Column>
29
+ <ScreenNameForm />
30
+ </Layout.Column>
31
+ </Layout>
32
+ );
33
+ ```
34
+
35
+ For edit forms that need existing data, co-locate data fetching in the page component:
36
+
37
+ ```tsx
38
+ const EditPage = () => {
39
+ const { id } = useParams();
40
+ const [{ data, error, fetching }] = useQuery({
41
+ query: ResourceQuery,
42
+ variables: { id: id! },
43
+ });
44
+
45
+ if (fetching) return <Loading />;
46
+ if (error || !data?.resource) return <ErrorFallback ... />;
47
+
48
+ return (
49
+ <Layout columns={1} title="Edit Resource">
50
+ <Layout.Column>
51
+ <EditResourceForm resource={data.resource} />
52
+ </Layout.Column>
53
+ </Layout>
54
+ );
55
+ };
56
+ ```
57
+
58
+ ## Form Component (components/{screen-name}-form.tsx)
59
+
60
+ ### Technology Stack
61
+
62
+ - `react-hook-form` — form state management
63
+ - `zod` + `@hookform/resolvers/zod` — validation
64
+ - `useMutation` (urql) — GraphQL mutation
65
+ - `useNavigate` (@tailor-platform/app-shell) — post-submit navigation
66
+
67
+ ### Pattern
68
+
69
+ ```tsx
70
+ const formSchema = z.object({
71
+ title: z.string().min(1, "Title is required"),
72
+ description: z.string().optional(),
73
+ });
74
+
75
+ type FormValues = z.infer<typeof formSchema>;
76
+
77
+ export const ScreenNameForm = () => {
78
+ const navigate = useNavigate();
79
+ const [, createResource] = useMutation(CreateMutation);
80
+
81
+ const form = useForm<FormValues>({
82
+ resolver: zodResolver(formSchema),
83
+ defaultValues: { title: "", description: "" },
84
+ });
85
+
86
+ const onSubmit = (values: FormValues) => {
87
+ void createResource({ input: values }).then((result) => {
88
+ if (!result.error) {
89
+ void navigate("..");
90
+ }
91
+ });
92
+ };
93
+
94
+ return (
95
+ <Form {...form}>
96
+ <form onSubmit={(e) => void form.handleSubmit(onSubmit)(e)} className="max-w-md space-y-4">
97
+ <FormField
98
+ control={form.control}
99
+ name="title"
100
+ render={({ field }) => (
101
+ <FormItem>
102
+ <FormLabel>Title</FormLabel>
103
+ <FormControl>
104
+ <Input placeholder="Enter title" {...field} />
105
+ </FormControl>
106
+ <FormMessage />
107
+ </FormItem>
108
+ )}
109
+ />
110
+ <div className="flex gap-2">
111
+ <Button type="submit">Create</Button>
112
+ <Button type="button" variant="outline" onClick={() => void navigate("..")}>
113
+ Cancel
114
+ </Button>
115
+ </div>
116
+ </form>
117
+ </Form>
118
+ );
119
+ };
120
+ ```
121
+
122
+ ## Field Type Mapping
123
+
124
+ | Field Type | Component | Zod Schema |
125
+ | ---------- | ------------------------------ | ------------------------------- |
126
+ | Text | `<Input />` | `z.string()` |
127
+ | Textarea | `<textarea className="..." />` | `z.string()` |
128
+ | Dropdown | `<Select />` | `z.string()` or `z.enum([...])` |
129
+ | Date | `<Input type="date" />` | `z.string()` (ISO format) |
130
+ | Number | `<Input type="number" />` | `z.coerce.number()` |
131
+ | Email | `<Input type="email" />` | `z.string().email()` |
132
+ | Checkbox | `<Checkbox />` | `z.boolean()` |
133
+ | Radio | `<RadioGroup />` | `z.enum([...])` |
134
+
135
+ ## Validation Mapping
136
+
137
+ - **Required: Yes** → `.min(1, "Field is required")` (string) / `.positive()` (number)
138
+ - **Required: No** → `.optional()`
139
+
140
+ ## Key Points
141
+
142
+ - Set `defaultValues` for all fields (empty string, false, etc.)
143
+ - Navigate to `".."` after successful mutation
144
+ - Cancel button must use `type="button"` to prevent form submit
145
+ - For edit forms, accept fragment data as props and pre-fill `defaultValues`