@nextsparkjs/ai-workflow 0.1.0-beta.100

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 (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/claude/_docs/workflows-optimizations.md +359 -0
  4. package/claude/agents/api-tester.md +634 -0
  5. package/claude/agents/architecture-supervisor.md +1351 -0
  6. package/claude/agents/backend-developer.md +997 -0
  7. package/claude/agents/backend-validator.md +417 -0
  8. package/claude/agents/bdd-docs-writer.md +737 -0
  9. package/claude/agents/block-developer.md +677 -0
  10. package/claude/agents/code-reviewer.md +1432 -0
  11. package/claude/agents/db-developer.md +721 -0
  12. package/claude/agents/db-validator.md +407 -0
  13. package/claude/agents/demo-video-generator.md +493 -0
  14. package/claude/agents/documentation-writer.md +1268 -0
  15. package/claude/agents/frontend-developer.md +1234 -0
  16. package/claude/agents/frontend-validator.md +777 -0
  17. package/claude/agents/functional-validator.md +630 -0
  18. package/claude/agents/mock-analyst.md +387 -0
  19. package/claude/agents/product-manager.md +963 -0
  20. package/claude/agents/qa-automation.md +1762 -0
  21. package/claude/agents/release-manager.md +634 -0
  22. package/claude/agents/selectors-translator.md +262 -0
  23. package/claude/agents/unit-test-writer.md +785 -0
  24. package/claude/agents/visual-comparator.md +329 -0
  25. package/claude/agents/workflow-maintainer.md +352 -0
  26. package/claude/commands/do/README.md +88 -0
  27. package/claude/commands/do/create-api.md +64 -0
  28. package/claude/commands/do/create-entity.md +66 -0
  29. package/claude/commands/do/create-migration.md +64 -0
  30. package/claude/commands/do/create-plugin.md +56 -0
  31. package/claude/commands/do/create-theme.md +70 -0
  32. package/claude/commands/do/mock-data.md +67 -0
  33. package/claude/commands/do/reset-db.md +71 -0
  34. package/claude/commands/do/setup-scheduled-action.md +75 -0
  35. package/claude/commands/do/sync-code-review.md +117 -0
  36. package/claude/commands/do/update-selectors.md +112 -0
  37. package/claude/commands/do/use-skills.md +90 -0
  38. package/claude/commands/do/validate-blocks.md +69 -0
  39. package/claude/commands/how-to/README.md +261 -0
  40. package/claude/commands/how-to/add-metadata.md +692 -0
  41. package/claude/commands/how-to/add-taxonomies.md +806 -0
  42. package/claude/commands/how-to/add-translations.md +571 -0
  43. package/claude/commands/how-to/create-api.md +577 -0
  44. package/claude/commands/how-to/create-block.md +575 -0
  45. package/claude/commands/how-to/create-child-entities.md +771 -0
  46. package/claude/commands/how-to/create-entity.md +597 -0
  47. package/claude/commands/how-to/create-migrations.md +605 -0
  48. package/claude/commands/how-to/create-plugin.md +654 -0
  49. package/claude/commands/how-to/customize-app.md +481 -0
  50. package/claude/commands/how-to/customize-dashboard.md +553 -0
  51. package/claude/commands/how-to/customize-theme.md +438 -0
  52. package/claude/commands/how-to/define-features-flows.md +632 -0
  53. package/claude/commands/how-to/deploy.md +507 -0
  54. package/claude/commands/how-to/handle-file-uploads.md +746 -0
  55. package/claude/commands/how-to/implement-search.md +1001 -0
  56. package/claude/commands/how-to/install-plugins.md +352 -0
  57. package/claude/commands/how-to/manage-test-coverage.md +984 -0
  58. package/claude/commands/how-to/run-tests.md +400 -0
  59. package/claude/commands/how-to/set-app-languages.md +601 -0
  60. package/claude/commands/how-to/set-plans-and-permissions.md +575 -0
  61. package/claude/commands/how-to/set-scheduled-actions.md +527 -0
  62. package/claude/commands/how-to/set-user-roles-and-permissions.md +550 -0
  63. package/claude/commands/how-to/setup-authentication.md +388 -0
  64. package/claude/commands/how-to/setup-claude-code.md +440 -0
  65. package/claude/commands/how-to/setup-database.md +274 -0
  66. package/claude/commands/how-to/setup-email-providers.md +598 -0
  67. package/claude/commands/how-to/setup-mobile-dev.md +627 -0
  68. package/claude/commands/how-to/start.md +500 -0
  69. package/claude/commands/how-to/use-devtools.md +639 -0
  70. package/claude/commands/how-to/use-superadmin.md +622 -0
  71. package/claude/commands/session/README.md +193 -0
  72. package/claude/commands/session/block-create.md +190 -0
  73. package/claude/commands/session/block-list.md +203 -0
  74. package/claude/commands/session/block-update.md +192 -0
  75. package/claude/commands/session/block-validate.md +218 -0
  76. package/claude/commands/session/changelog.md +115 -0
  77. package/claude/commands/session/close.md +225 -0
  78. package/claude/commands/session/commit.md +174 -0
  79. package/claude/commands/session/db-entity.md +206 -0
  80. package/claude/commands/session/db-fix.md +212 -0
  81. package/claude/commands/session/db-sample.md +206 -0
  82. package/claude/commands/session/demo.md +178 -0
  83. package/claude/commands/session/doc-bdd.md +207 -0
  84. package/claude/commands/session/doc-feature.md +218 -0
  85. package/claude/commands/session/doc-read.md +225 -0
  86. package/claude/commands/session/execute.md +204 -0
  87. package/claude/commands/session/explain.md +202 -0
  88. package/claude/commands/session/fix-bug.md +210 -0
  89. package/claude/commands/session/fix-build.md +182 -0
  90. package/claude/commands/session/fix-test.md +189 -0
  91. package/claude/commands/session/pending.md +232 -0
  92. package/claude/commands/session/refine.md +188 -0
  93. package/claude/commands/session/resume.md +192 -0
  94. package/claude/commands/session/review.md +192 -0
  95. package/claude/commands/session/scope-change.md +181 -0
  96. package/claude/commands/session/start-blocks.md +347 -0
  97. package/claude/commands/session/start.md +604 -0
  98. package/claude/commands/session/status.md +169 -0
  99. package/claude/commands/session/test-fix.md +221 -0
  100. package/claude/commands/session/test-run.md +203 -0
  101. package/claude/commands/session/test-write.md +242 -0
  102. package/claude/commands/session/validate.md +162 -0
  103. package/claude/config/context.json +40 -0
  104. package/claude/config/github.json +69 -0
  105. package/claude/config/github.schema.json +106 -0
  106. package/claude/config/team.json +46 -0
  107. package/claude/config/team.schema.json +106 -0
  108. package/claude/config/workspace.json +43 -0
  109. package/claude/config/workspace.schema.json +75 -0
  110. package/claude/skills/README.md +228 -0
  111. package/claude/skills/accessibility/SKILL.md +573 -0
  112. package/claude/skills/api-bypass-layers/SKILL.md +550 -0
  113. package/claude/skills/asana-integration/SKILL.md +499 -0
  114. package/claude/skills/better-auth/SKILL.md +666 -0
  115. package/claude/skills/billing-subscriptions/SKILL.md +660 -0
  116. package/claude/skills/block-decision-matrix/SKILL.md +359 -0
  117. package/claude/skills/clickup-integration/SKILL.md +434 -0
  118. package/claude/skills/core-theme-responsibilities/SKILL.md +485 -0
  119. package/claude/skills/create-plugin/SKILL.md +425 -0
  120. package/claude/skills/create-theme/SKILL.md +331 -0
  121. package/claude/skills/cypress-api/SKILL.md +511 -0
  122. package/claude/skills/cypress-api/scripts/generate-api-controller.py +329 -0
  123. package/claude/skills/cypress-api/scripts/generate-api-test.py +930 -0
  124. package/claude/skills/cypress-e2e/SKILL.md +526 -0
  125. package/claude/skills/cypress-e2e/scripts/extract-selectors.py +383 -0
  126. package/claude/skills/cypress-e2e/scripts/generate-uat-test.py +788 -0
  127. package/claude/skills/cypress-selectors/SKILL.md +309 -0
  128. package/claude/skills/cypress-selectors/scripts/extract-missing.py +243 -0
  129. package/claude/skills/cypress-selectors/scripts/generate-block-selectors.py +283 -0
  130. package/claude/skills/cypress-selectors/scripts/validate-selectors.py +145 -0
  131. package/claude/skills/database-migrations/SKILL.md +335 -0
  132. package/claude/skills/database-migrations/scripts/generate-sample-data.py +284 -0
  133. package/claude/skills/database-migrations/scripts/validate-migration.py +323 -0
  134. package/claude/skills/design-system/SKILL.md +682 -0
  135. package/claude/skills/documentation/SKILL.md +540 -0
  136. package/claude/skills/entity-api/SKILL.md +482 -0
  137. package/claude/skills/entity-system/SKILL.md +635 -0
  138. package/claude/skills/entity-system/scripts/generate-child-migration.py +298 -0
  139. package/claude/skills/entity-system/scripts/generate-metas-migration.py +233 -0
  140. package/claude/skills/entity-system/scripts/generate-migration.py +382 -0
  141. package/claude/skills/entity-system/scripts/generate-sample-data.py +418 -0
  142. package/claude/skills/entity-system/scripts/scaffold-entity.py +661 -0
  143. package/claude/skills/github/SKILL.md +467 -0
  144. package/claude/skills/i18n-nextintl/SKILL.md +302 -0
  145. package/claude/skills/i18n-nextintl/scripts/add-translation.py +243 -0
  146. package/claude/skills/i18n-nextintl/scripts/extract-hardcoded.py +246 -0
  147. package/claude/skills/i18n-nextintl/scripts/validate-translations.py +260 -0
  148. package/claude/skills/impact-analysis/SKILL.md +203 -0
  149. package/claude/skills/jest-unit/SKILL.md +306 -0
  150. package/claude/skills/jest-unit/references/component-testing.md +371 -0
  151. package/claude/skills/jest-unit/references/mocking-patterns.md +380 -0
  152. package/claude/skills/jest-unit/references/service-hook-testing.md +454 -0
  153. package/claude/skills/jira-integration/SKILL.md +539 -0
  154. package/claude/skills/media-library/SKILL.md +743 -0
  155. package/claude/skills/mock-analysis/SKILL.md +276 -0
  156. package/claude/skills/monorepo-architecture/SKILL.md +162 -0
  157. package/claude/skills/nextjs-api-development/SKILL.md +364 -0
  158. package/claude/skills/nextjs-api-development/scripts/generate-crud-tests.py +456 -0
  159. package/claude/skills/nextjs-api-development/scripts/scaffold-endpoint.py +481 -0
  160. package/claude/skills/nextjs-api-development/scripts/validate-api.py +283 -0
  161. package/claude/skills/notion-integration/SKILL.md +641 -0
  162. package/claude/skills/npm-development-workflow/SKILL.md +480 -0
  163. package/claude/skills/page-builder-blocks/SKILL.md +530 -0
  164. package/claude/skills/page-builder-blocks/scripts/scaffold-block.py +444 -0
  165. package/claude/skills/permissions-system/SKILL.md +619 -0
  166. package/claude/skills/plugins/SKILL.md +340 -0
  167. package/claude/skills/plugins/references/plugin-templates.md +414 -0
  168. package/claude/skills/plugins/references/plugin-testing.md +353 -0
  169. package/claude/skills/plugins/references/plugin-types.md +198 -0
  170. package/claude/skills/plugins/scripts/scaffold-plugin.py +443 -0
  171. package/claude/skills/pom-patterns/SKILL.md +452 -0
  172. package/claude/skills/pom-patterns/scripts/generate-pom.py +392 -0
  173. package/claude/skills/rate-limiting/SKILL.md +342 -0
  174. package/claude/skills/react-best-practices/AGENTS.md +2410 -0
  175. package/claude/skills/react-best-practices/README.md +123 -0
  176. package/claude/skills/react-best-practices/SKILL.md +125 -0
  177. package/claude/skills/react-best-practices/metadata.json +15 -0
  178. package/claude/skills/react-best-practices/rules/_sections.md +46 -0
  179. package/claude/skills/react-best-practices/rules/_template.md +28 -0
  180. package/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  181. package/claude/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
  182. package/claude/skills/react-best-practices/rules/async-api-routes.md +38 -0
  183. package/claude/skills/react-best-practices/rules/async-defer-await.md +80 -0
  184. package/claude/skills/react-best-practices/rules/async-dependencies.md +36 -0
  185. package/claude/skills/react-best-practices/rules/async-parallel.md +28 -0
  186. package/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
  187. package/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
  188. package/claude/skills/react-best-practices/rules/bundle-conditional.md +31 -0
  189. package/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
  190. package/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  191. package/claude/skills/react-best-practices/rules/bundle-preload.md +50 -0
  192. package/claude/skills/react-best-practices/rules/client-event-listeners.md +74 -0
  193. package/claude/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
  194. package/claude/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
  195. package/claude/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
  196. package/claude/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
  197. package/claude/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
  198. package/claude/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  199. package/claude/skills/react-best-practices/rules/js-cache-storage.md +70 -0
  200. package/claude/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  201. package/claude/skills/react-best-practices/rules/js-early-exit.md +50 -0
  202. package/claude/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  203. package/claude/skills/react-best-practices/rules/js-index-maps.md +37 -0
  204. package/claude/skills/react-best-practices/rules/js-length-check-first.md +49 -0
  205. package/claude/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  206. package/claude/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  207. package/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  208. package/claude/skills/react-best-practices/rules/rendering-activity.md +26 -0
  209. package/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  210. package/claude/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
  211. package/claude/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  212. package/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  213. package/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  214. package/claude/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  215. package/claude/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  216. package/claude/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  217. package/claude/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  218. package/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
  219. package/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  220. package/claude/skills/react-best-practices/rules/rerender-memo.md +44 -0
  221. package/claude/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  222. package/claude/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
  223. package/claude/skills/react-best-practices/rules/server-cache-lru.md +41 -0
  224. package/claude/skills/react-best-practices/rules/server-cache-react.md +76 -0
  225. package/claude/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
  226. package/claude/skills/react-best-practices/rules/server-serialization.md +38 -0
  227. package/claude/skills/react-patterns/SKILL.md +688 -0
  228. package/claude/skills/registry-system/SKILL.md +331 -0
  229. package/claude/skills/scheduled-actions/SKILL.md +671 -0
  230. package/claude/skills/scope-enforcement/SKILL.md +542 -0
  231. package/claude/skills/scope-enforcement/scripts/validate-scope.py +357 -0
  232. package/claude/skills/server-actions/SKILL.md +493 -0
  233. package/claude/skills/service-layer/SKILL.md +587 -0
  234. package/claude/skills/session-management/SKILL.md +266 -0
  235. package/claude/skills/session-management/scripts/create-session.py +166 -0
  236. package/claude/skills/session-management/scripts/iteration-close.sh +105 -0
  237. package/claude/skills/session-management/scripts/iteration-init.sh +180 -0
  238. package/claude/skills/session-management/scripts/session-archive.sh +87 -0
  239. package/claude/skills/session-management/scripts/session-close.sh +133 -0
  240. package/claude/skills/session-management/scripts/session-init.sh +225 -0
  241. package/claude/skills/session-management/scripts/session-list.sh +163 -0
  242. package/claude/skills/session-management/scripts/split-plan.sh +116 -0
  243. package/claude/skills/shadcn-components/SKILL.md +586 -0
  244. package/claude/skills/shadcn-theming/SKILL.md +446 -0
  245. package/claude/skills/suspense-loading/SKILL.md +280 -0
  246. package/claude/skills/tailwind-theming/SKILL.md +507 -0
  247. package/claude/skills/tanstack-query/SKILL.md +608 -0
  248. package/claude/skills/test-coverage/SKILL.md +239 -0
  249. package/claude/skills/web-design-guidelines/SKILL.md +39 -0
  250. package/claude/skills/zod-validation/SKILL.md +537 -0
  251. package/claude/templates/blocks/progress.md +86 -0
  252. package/claude/templates/iteration/changes.md +61 -0
  253. package/claude/templates/iteration/progress.md +55 -0
  254. package/claude/templates/log.md +31 -0
  255. package/claude/templates/story/context.md +77 -0
  256. package/claude/templates/story/pendings.md +37 -0
  257. package/claude/templates/story/plan.md +299 -0
  258. package/claude/templates/story/requirements.md +109 -0
  259. package/claude/templates/story/scope.json +10 -0
  260. package/claude/templates/story/tests.md +91 -0
  261. package/claude/templates/task/progress.md +58 -0
  262. package/claude/templates/task/requirements.md +54 -0
  263. package/claude/workflows/README.md +154 -0
  264. package/claude/workflows/blocks.md +614 -0
  265. package/claude/workflows/story.md +1207 -0
  266. package/claude/workflows/task.md +927 -0
  267. package/claude/workflows/tweak.md +527 -0
  268. package/cursor/.gitkeep +0 -0
  269. package/package.json +35 -0
  270. package/scripts/postinstall.mjs +198 -0
  271. package/scripts/setup.mjs +282 -0
  272. package/scripts/sync.mjs +209 -0
@@ -0,0 +1,743 @@
1
+ ---
2
+ name: media-library
3
+ description: |
4
+ WordPress-style media management system for this Next.js application.
5
+ Covers MediaService CRUD, file upload, tag system, duplicate detection,
6
+ MediaLibrary modal, MediaSelector form field, and block editor integration.
7
+ Use this skill when working with media uploads, browsing, or selection.
8
+ allowed-tools: Read, Glob, Grep, Bash
9
+ version: 1.0.0
10
+ ---
11
+
12
+ # Media Library Skill
13
+
14
+ WordPress-style media management system with full CRUD API, reusable modal, form field component, and block editor integration.
15
+
16
+ ## Architecture Overview
17
+
18
+ ```
19
+ MEDIA LIBRARY SYSTEM:
20
+
21
+ Core Layer (packages/core/):
22
+ ├── src/
23
+ │ ├── components/media/
24
+ │ │ ├── MediaLibrary.tsx # Main modal (browse, upload, select)
25
+ │ │ ├── MediaGrid.tsx # Grid view with thumbnails
26
+ │ │ ├── MediaList.tsx # List/table view
27
+ │ │ ├── MediaCard.tsx # Individual media card
28
+ │ │ ├── MediaToolbar.tsx # Search, filter, sort, view toggle
29
+ │ │ ├── MediaDetailPanel.tsx # Right sidebar detail/edit panel
30
+ │ │ ├── MediaUploadZone.tsx # Drag & drop upload area
31
+ │ │ ├── MediaSelector.tsx # Form field component for entities
32
+ │ │ ├── MediaTagFilter.tsx # Tag filter chips
33
+ │ │ └── index.ts # Re-exports
34
+ │ ├── hooks/
35
+ │ │ ├── useMedia.ts # TanStack Query hooks (CRUD + tags)
36
+ │ │ └── useMediaUpload.ts # Upload hook with progress tracking
37
+ │ ├── lib/
38
+ │ │ ├── media/
39
+ │ │ │ ├── types.ts # Media, MediaTag, MediaListOptions
40
+ │ │ │ ├── schemas.ts # Zod validation schemas
41
+ │ │ │ └── utils.ts # formatFileSize, getMediaType, etc.
42
+ │ │ └── services/
43
+ │ │ └── media.service.ts # MediaService (CRUD, tags, duplicates)
44
+ │ └── types/
45
+ │ └── blocks.ts # FieldType includes 'media-library'
46
+
47
+ ├── migrations/
48
+ │ └── 021_media.sql # Media + media_tags + media_tag_relations
49
+
50
+ API Layer (apps/dev/app/api/v1/):
51
+ ├── media/
52
+ │ ├── route.ts # GET (list), POST (create)
53
+ │ ├── upload/route.ts # POST (file upload)
54
+ │ ├── check-duplicates/route.ts # POST (hash check)
55
+ │ └── [id]/
56
+ │ ├── route.ts # GET, PATCH, DELETE
57
+ │ └── tags/route.ts # GET, POST, DELETE media tags
58
+ └── media-tags/
59
+ └── route.ts # GET all tags
60
+
61
+ Dashboard (apps/dev/app/dashboard/(main)/media/):
62
+ └── page.tsx # Dashboard media page
63
+
64
+ Flow:
65
+ Upload → API → MediaService → DB → TanStack Query Cache → UI
66
+ ```
67
+
68
+ > **Context-Aware Paths:** Core layer components and services are in `packages/core/`.
69
+ > API routes are in the app layer. Dashboard page is theme-provided.
70
+ > See `core-theme-responsibilities` skill for complete rules.
71
+
72
+ ## When to Use This Skill
73
+
74
+ - Adding media upload functionality to a feature
75
+ - Integrating media selection into entity forms
76
+ - Adding `type: 'media-library'` fields to page builder blocks
77
+ - Working with the MediaLibrary modal or MediaSelector component
78
+ - Implementing media tag filtering or organization
79
+ - Debugging upload, duplicate detection, or media API issues
80
+ - Configuring upload size limits or accepted file types
81
+
82
+ ## Media Type Definition
83
+
84
+ ```typescript
85
+ interface Media {
86
+ id: string
87
+ userId: string
88
+ teamId: string
89
+ filename: string
90
+ originalFilename: string
91
+ mimeType: string
92
+ fileSize: number
93
+ url: string
94
+ thumbnailUrl?: string
95
+ width?: number
96
+ height?: number
97
+ title?: string
98
+ alt?: string
99
+ caption?: string
100
+ hash?: string
101
+ status: 'active' | 'archived'
102
+ metadata?: Record<string, unknown>
103
+ createdAt: string
104
+ updatedAt: string
105
+ }
106
+
107
+ interface MediaTag {
108
+ id: string
109
+ name: string
110
+ slug: string
111
+ createdAt: string
112
+ }
113
+
114
+ interface MediaListOptions {
115
+ limit?: number // Default: 50
116
+ offset?: number // Default: 0
117
+ orderBy?: string // Default: 'createdAt'
118
+ orderDir?: 'asc' | 'desc' // Default: 'desc'
119
+ type?: 'all' | 'image' | 'video'
120
+ search?: string // Searches filename, title, alt
121
+ tagIds?: string[] // Filter by tag IDs
122
+ }
123
+ ```
124
+
125
+ ## API Endpoints
126
+
127
+ | Method | Endpoint | Description | Auth Scope |
128
+ |--------|----------|-------------|------------|
129
+ | GET | `/api/v1/media` | List media (paginated, filterable) | `media:read` |
130
+ | POST | `/api/v1/media` | Create media record | `media:write` |
131
+ | POST | `/api/v1/media/upload` | Upload files | `media:write` |
132
+ | POST | `/api/v1/media/check-duplicates` | Check for duplicates by hash | `media:read` |
133
+ | GET | `/api/v1/media/[id]` | Get single media item | `media:read` |
134
+ | PATCH | `/api/v1/media/[id]` | Update media metadata | `media:write` |
135
+ | DELETE | `/api/v1/media/[id]` | Delete media item | `media:delete` |
136
+ | GET | `/api/v1/media/[id]/tags` | Get tags for a media item | `media:read` |
137
+ | POST | `/api/v1/media/[id]/tags` | Add tags to media | `media:write` |
138
+ | DELETE | `/api/v1/media/[id]/tags` | Remove tags from media | `media:write` |
139
+ | GET | `/api/v1/media-tags` | List all available tags | `media:read` |
140
+
141
+ All endpoints support dual authentication (session cookie + API key via `x-api-key` header).
142
+
143
+ ### Query Parameters for GET /api/v1/media
144
+
145
+ | Param | Type | Default | Description |
146
+ |-------|------|---------|-------------|
147
+ | `limit` | number | 50 | Items per page |
148
+ | `offset` | number | 0 | Pagination offset |
149
+ | `orderBy` | string | `createdAt` | Sort field (`createdAt`, `filename`, `fileSize`, `mimeType`) |
150
+ | `orderDir` | string | `desc` | Sort direction (`asc`, `desc`) |
151
+ | `type` | string | `all` | Filter: `all`, `image`, `video` |
152
+ | `search` | string | - | Search in filename, title, alt |
153
+ | `tagIds` | string | - | Comma-separated tag IDs |
154
+
155
+ ## MediaService
156
+
157
+ Static class following the service-layer pattern with RLS integration:
158
+
159
+ ```typescript
160
+ // core/lib/services/media.service.ts
161
+
162
+ export class MediaService {
163
+ // CRUD operations
164
+ static async getById(id: string, userId: string): Promise<Media | null>
165
+ static async list(userId: string, options?: MediaListOptions): Promise<ListResult<Media>>
166
+ static async create(userId: string, teamId: string, data: CreateMedia): Promise<Media>
167
+ static async update(id: string, userId: string, data: UpdateMedia): Promise<Media>
168
+ static async delete(id: string, userId: string): Promise<boolean>
169
+
170
+ // Tag operations
171
+ static async getTags(mediaId: string, userId: string): Promise<MediaTag[]>
172
+ static async addTag(mediaId: string, tagId: string, userId: string): Promise<void>
173
+ static async removeTag(mediaId: string, tagId: string, userId: string): Promise<void>
174
+ static async getAllTags(userId: string): Promise<MediaTag[]>
175
+
176
+ // Duplicate detection
177
+ static async checkDuplicates(hashes: string[], userId: string): Promise<Media[]>
178
+ }
179
+ ```
180
+
181
+ ## TanStack Query Hooks
182
+
183
+ ### Query Hooks
184
+
185
+ ```typescript
186
+ import {
187
+ useMediaList,
188
+ useMediaItem,
189
+ useMediaTags,
190
+ useMediaItemTags,
191
+ } from '@/core/hooks/useMedia'
192
+
193
+ // List media with filters (paginated)
194
+ const { data, isLoading, error } = useMediaList({
195
+ limit: 50,
196
+ offset: 0,
197
+ type: 'image',
198
+ search: 'hero',
199
+ orderBy: 'createdAt',
200
+ orderDir: 'desc',
201
+ tagIds: ['tag-1', 'tag-2'],
202
+ })
203
+
204
+ // Single media item (enabled when id is truthy)
205
+ const { data: media } = useMediaItem(selectedId)
206
+
207
+ // All available tags
208
+ const { data: tags } = useMediaTags()
209
+
210
+ // Tags for a specific media item
211
+ const { data: mediaTags } = useMediaItemTags(mediaId)
212
+ ```
213
+
214
+ ### Mutation Hooks
215
+
216
+ ```typescript
217
+ import {
218
+ useCreateMedia,
219
+ useUpdateMedia,
220
+ useDeleteMedia,
221
+ useAddMediaTag,
222
+ useRemoveMediaTag,
223
+ } from '@/core/hooks/useMedia'
224
+
225
+ // Create media record
226
+ const createMutation = useCreateMedia()
227
+ await createMutation.mutateAsync({ filename, url, mimeType, fileSize })
228
+
229
+ // Update media metadata
230
+ const updateMutation = useUpdateMedia()
231
+ await updateMutation.mutateAsync({ id: 'media-123', title: 'New Title', alt: 'Alt text' })
232
+
233
+ // Delete media
234
+ const deleteMutation = useDeleteMedia()
235
+ await deleteMutation.mutateAsync('media-123')
236
+
237
+ // Tag management
238
+ const addTagMutation = useAddMediaTag()
239
+ await addTagMutation.mutateAsync({ mediaId: 'media-123', tagId: 'tag-456' })
240
+
241
+ const removeTagMutation = useRemoveMediaTag()
242
+ await removeTagMutation.mutateAsync({ mediaId: 'media-123', tagId: 'tag-456' })
243
+ ```
244
+
245
+ ### Upload Hook
246
+
247
+ ```typescript
248
+ import { useMediaUpload } from '@/core/hooks/useMediaUpload'
249
+
250
+ const { upload, progress, isUploading, error } = useMediaUpload()
251
+
252
+ // Upload a file with progress tracking
253
+ const media = await upload(file)
254
+ console.log(`Upload progress: ${progress}%`)
255
+ ```
256
+
257
+ ### Query Key Conventions
258
+
259
+ ```typescript
260
+ // Media query keys follow the standard pattern:
261
+ ['media', filters] // Media list with filters
262
+ ['media', id] // Single media item
263
+ ['media-tags'] // All tags
264
+ ['media', mediaId, 'tags'] // Tags for specific media
265
+ ```
266
+
267
+ ## Component Usage
268
+
269
+ ### MediaLibrary Modal
270
+
271
+ Full-featured modal for browsing, uploading, and selecting media:
272
+
273
+ ```typescript
274
+ import { MediaLibrary } from '@/core/components/media'
275
+
276
+ // Single selection mode
277
+ <MediaLibrary
278
+ isOpen={isModalOpen}
279
+ onClose={() => setIsModalOpen(false)}
280
+ onSelect={(media: Media) => {
281
+ setSelectedImage(media.url)
282
+ }}
283
+ mode="single"
284
+ allowedTypes={['image']}
285
+ />
286
+
287
+ // Multiple selection mode
288
+ <MediaLibrary
289
+ isOpen={isModalOpen}
290
+ onClose={() => setIsModalOpen(false)}
291
+ onSelect={(mediaItems: Media[]) => {
292
+ setGalleryImages(mediaItems.map(m => m.url))
293
+ }}
294
+ mode="multiple"
295
+ allowedTypes={['image', 'video']}
296
+ maxSelections={10}
297
+ />
298
+ ```
299
+
300
+ **MediaLibrary Props:**
301
+
302
+ | Prop | Type | Required | Description |
303
+ |------|------|----------|-------------|
304
+ | `isOpen` | `boolean` | Yes | Controls modal visibility |
305
+ | `onClose` | `() => void` | Yes | Callback when modal closes |
306
+ | `onSelect` | `(media: Media \| Media[]) => void` | Yes | Callback with selected media |
307
+ | `mode` | `'single' \| 'multiple'` | No | Selection mode (default: `'single'`) |
308
+ | `allowedTypes` | `string[]` | No | Restrict to `['image']`, `['video']`, or both |
309
+ | `maxSelections` | `number` | No | Max items in multiple mode |
310
+
311
+ ### MediaSelector Form Field
312
+
313
+ Drop-in form component for entity fields:
314
+
315
+ ```typescript
316
+ import { MediaSelector } from '@/core/components/media'
317
+
318
+ // In an entity form
319
+ <MediaSelector
320
+ value={formData.featuredImageId}
321
+ onChange={(id) => setFormData({ ...formData, featuredImageId: id })}
322
+ type="image"
323
+ />
324
+
325
+ // For video selection
326
+ <MediaSelector
327
+ value={formData.videoId}
328
+ onChange={(id) => setFormData({ ...formData, videoId: id })}
329
+ type="video"
330
+ />
331
+ ```
332
+
333
+ **MediaSelector Props:**
334
+
335
+ | Prop | Type | Required | Description |
336
+ |------|------|----------|-------------|
337
+ | `value` | `string \| null` | Yes | Current media ID |
338
+ | `onChange` | `(id: string \| null) => void` | Yes | Callback when selection changes |
339
+ | `type` | `'image' \| 'video'` | No | Restrict selectable media type |
340
+
341
+ ## Block Editor Integration
342
+
343
+ ### Field Type: 'media-library'
344
+
345
+ In block field definitions, use `type: 'media-library'` to open the full MediaLibrary modal instead of a simple file input:
346
+
347
+ ```typescript
348
+ // contents/themes/{theme}/blocks/hero/fields.ts
349
+ import type { FieldDefinition } from '@/core/types/blocks'
350
+
351
+ const customDesignFields: FieldDefinition[] = [
352
+ {
353
+ name: 'backgroundImage',
354
+ label: 'Background Image',
355
+ type: 'media-library', // Opens MediaLibrary modal
356
+ tab: 'design',
357
+ },
358
+ ]
359
+ ```
360
+
361
+ ### How MediaLibraryField Works
362
+
363
+ The `MediaLibraryField` component renders based on state:
364
+
365
+ 1. **Empty state** - Shows "Browse Media Library" prompt button
366
+ 2. **On click** - Opens the full `MediaLibrary` modal
367
+ 3. **After selection** - Shows image preview with hover overlay (Change / Remove buttons)
368
+ 4. **Storage** - Stores the URL string (not media ID) in block data
369
+
370
+ ### In Array Sub-Fields
371
+
372
+ Media library fields also work inside array (repeatable) items:
373
+
374
+ ```typescript
375
+ // Array field with media sub-field
376
+ {
377
+ name: 'slides',
378
+ label: 'Slides',
379
+ type: 'array',
380
+ tab: 'content',
381
+ itemFields: [
382
+ { name: 'image', label: 'Slide Image', type: 'media-library', tab: 'content' },
383
+ { name: 'title', label: 'Title', type: 'text', tab: 'content' },
384
+ { name: 'description', label: 'Description', type: 'textarea', tab: 'content' },
385
+ ],
386
+ }
387
+ ```
388
+
389
+ ### Block Schema with Media
390
+
391
+ ```typescript
392
+ // contents/themes/{theme}/blocks/hero/schema.ts
393
+ import { z } from 'zod'
394
+ import { baseBlockSchema } from '@/core/types/blocks'
395
+
396
+ export const schema = baseBlockSchema.merge(z.object({
397
+ backgroundImage: z.string().url().optional(), // URL from MediaLibrary
398
+ overlayOpacity: z.number().min(0).max(100).default(50),
399
+ }))
400
+ ```
401
+
402
+ ## Upload Configuration
403
+
404
+ ### app.config.ts
405
+
406
+ ```typescript
407
+ // contents/themes/{theme}/config/app.config.ts
408
+ export const appConfig = {
409
+ // ... other config
410
+
411
+ media: {
412
+ maxSizeMB: 10, // General max size
413
+ maxSizeImageMB: 10, // Image-specific max
414
+ maxSizeVideoMB: 50, // Video-specific max
415
+ acceptedTypes: ['image/*', 'video/*'],
416
+ allowedMimeTypes: [
417
+ 'image/jpeg',
418
+ 'image/png',
419
+ 'image/gif',
420
+ 'image/webp',
421
+ 'image/svg+xml',
422
+ 'video/mp4',
423
+ 'video/webm',
424
+ ],
425
+ },
426
+ }
427
+ ```
428
+
429
+ ### Upload Flow
430
+
431
+ ```
432
+ 1. User drops file on MediaUploadZone (or clicks to browse)
433
+ 2. Client-side validation: file size, MIME type
434
+ 3. Client computes file hash (SHA-256)
435
+ 4. POST /api/v1/media/check-duplicates with hash
436
+ 5. If duplicate found: prompt user (skip/replace/upload anyway)
437
+ 6. POST /api/v1/media/upload with FormData
438
+ 7. Server validates, stores file, creates DB record
439
+ 8. TanStack Query cache invalidated, UI updates
440
+ ```
441
+
442
+ ## Database Schema
443
+
444
+ ### Migration: 021_media.sql
445
+
446
+ ```sql
447
+ -- Media items table
448
+ CREATE TABLE IF NOT EXISTS "media" (
449
+ "id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
450
+ "userId" TEXT NOT NULL REFERENCES "user"(id),
451
+ "teamId" TEXT NOT NULL REFERENCES "team"(id),
452
+ "filename" VARCHAR(500) NOT NULL,
453
+ "originalFilename" VARCHAR(500) NOT NULL,
454
+ "mimeType" VARCHAR(100) NOT NULL,
455
+ "fileSize" INTEGER NOT NULL,
456
+ "url" TEXT NOT NULL,
457
+ "thumbnailUrl" TEXT,
458
+ "width" INTEGER,
459
+ "height" INTEGER,
460
+ "title" VARCHAR(500),
461
+ "alt" VARCHAR(500),
462
+ "caption" TEXT,
463
+ "hash" VARCHAR(128),
464
+ "status" VARCHAR(20) NOT NULL DEFAULT 'active',
465
+ "metadata" JSONB DEFAULT '{}'::jsonb,
466
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
467
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
468
+ );
469
+
470
+ -- Media tags table
471
+ CREATE TABLE IF NOT EXISTS "media_tags" (
472
+ "id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
473
+ "name" VARCHAR(100) NOT NULL,
474
+ "slug" VARCHAR(100) NOT NULL UNIQUE,
475
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now()
476
+ );
477
+
478
+ -- Many-to-many relation: media <-> tags
479
+ CREATE TABLE IF NOT EXISTS "media_tag_relations" (
480
+ "mediaId" TEXT NOT NULL REFERENCES "media"(id) ON DELETE CASCADE,
481
+ "tagId" TEXT NOT NULL REFERENCES "media_tags"(id) ON DELETE CASCADE,
482
+ PRIMARY KEY ("mediaId", "tagId")
483
+ );
484
+
485
+ -- Indexes
486
+ CREATE INDEX "idx_media_userId" ON "media"("userId");
487
+ CREATE INDEX "idx_media_teamId" ON "media"("teamId");
488
+ CREATE INDEX "idx_media_hash" ON "media"("hash");
489
+ CREATE INDEX "idx_media_status" ON "media"("status");
490
+ CREATE INDEX "idx_media_mimeType" ON "media"("mimeType");
491
+ CREATE INDEX "idx_media_tag_relations_tagId" ON "media_tag_relations"("tagId");
492
+ ```
493
+
494
+ ## Internationalization
495
+
496
+ ### i18n Namespace: `media`
497
+
498
+ 60+ translation keys in `en` and `es` locales.
499
+
500
+ ```typescript
501
+ // Usage in components
502
+ const t = useTranslations('media')
503
+
504
+ t('title') // "Media Library"
505
+ t('upload') // "Upload"
506
+ t('dragDrop') // "Drag & drop files here"
507
+ t('noResults') // "No media found"
508
+ t('confirmDelete') // "Are you sure you want to delete this media?"
509
+ t('filters.allTypes') // "All Types"
510
+ t('filters.images') // "Images"
511
+ t('filters.videos') // "Videos"
512
+ t('detail.title') // "Title"
513
+ t('detail.alt') // "Alt Text"
514
+ t('detail.caption') // "Caption"
515
+ t('detail.fileSize') // "File Size"
516
+ t('detail.dimensions') // "Dimensions"
517
+ t('detail.uploadedAt') // "Uploaded At"
518
+ t('tags.addTag') // "Add Tag"
519
+ t('tags.removeTag') // "Remove Tag"
520
+ ```
521
+
522
+ ### Block Editor i18n Keys
523
+
524
+ ```typescript
525
+ // Namespace: admin.blockEditor.form
526
+ t('changeImage') // "Change"
527
+ t('removeImage') // "Remove"
528
+ t('browseMedia') // "Browse Media Library"
529
+ ```
530
+
531
+ ## Testing
532
+
533
+ ### Cypress API Tests
534
+
535
+ File: `media-crud.cy.ts` (20+ test cases)
536
+
537
+ ```typescript
538
+ // Tests cover:
539
+ // - List media with pagination
540
+ // - List media with type filter (image, video)
541
+ // - List media with search
542
+ // - List media with tag filter
543
+ // - List media with sorting
544
+ // - Create media record
545
+ // - Upload file
546
+ // - Get single media
547
+ // - Update media metadata (title, alt, caption)
548
+ // - Delete media
549
+ // - Duplicate detection via hash
550
+ // - Add/remove tags
551
+ // - Permission checks (media:read, media:write, media:delete)
552
+ // - Dual auth (session + API key)
553
+ ```
554
+
555
+ ### Jest Unit Tests
556
+
557
+ ```
558
+ media.service.test.ts # Service layer unit tests
559
+ schemas.test.ts # Zod schema validation tests
560
+ utils.test.ts # Utility function tests (formatFileSize, getMediaType, etc.)
561
+ ```
562
+
563
+ ### data-cy Selectors
564
+
565
+ Defined in `media.selectors.ts` and `block-editor.selectors.ts`:
566
+
567
+ ```typescript
568
+ // media.selectors.ts
569
+ export const MEDIA_SELECTORS = {
570
+ library: {
571
+ modal: 'media-library-modal',
572
+ grid: 'media-grid',
573
+ list: 'media-list',
574
+ card: 'media-card',
575
+ toolbar: 'media-toolbar',
576
+ searchInput: 'media-search-input',
577
+ typeFilter: 'media-type-filter',
578
+ sortSelect: 'media-sort-select',
579
+ viewToggle: 'media-view-toggle',
580
+ uploadZone: 'media-upload-zone',
581
+ detailPanel: 'media-detail-panel',
582
+ selectBtn: 'media-select-btn',
583
+ deleteBtn: 'media-delete-btn',
584
+ },
585
+ selector: {
586
+ container: 'media-selector',
587
+ preview: 'media-selector-preview',
588
+ browseBtn: 'media-selector-browse',
589
+ removeBtn: 'media-selector-remove',
590
+ },
591
+ tags: {
592
+ filter: 'media-tag-filter',
593
+ chip: 'media-tag-chip',
594
+ addBtn: 'media-tag-add',
595
+ removeBtn: 'media-tag-remove',
596
+ },
597
+ } as const
598
+
599
+ // block-editor.selectors.ts (additions)
600
+ export const BLOCK_EDITOR_SELECTORS = {
601
+ // ... existing selectors
602
+ mediaField: {
603
+ container: 'block-media-field',
604
+ preview: 'block-media-preview',
605
+ browseBtn: 'block-media-browse',
606
+ changeBtn: 'block-media-change',
607
+ removeBtn: 'block-media-remove',
608
+ },
609
+ } as const
610
+ ```
611
+
612
+ ## Key Files Reference
613
+
614
+ | File | Purpose |
615
+ |------|---------|
616
+ | `core/src/components/media/MediaLibrary.tsx` | Main modal component |
617
+ | `core/src/components/media/MediaSelector.tsx` | Form field component |
618
+ | `core/src/components/media/MediaUploadZone.tsx` | Drag & drop upload |
619
+ | `core/src/hooks/useMedia.ts` | TanStack Query hooks (CRUD + tags) |
620
+ | `core/src/hooks/useMediaUpload.ts` | Upload with progress |
621
+ | `core/src/lib/media/types.ts` | TypeScript type definitions |
622
+ | `core/src/lib/media/schemas.ts` | Zod validation schemas |
623
+ | `core/src/lib/media/utils.ts` | Utility functions |
624
+ | `core/src/lib/services/media.service.ts` | MediaService (CRUD, tags, duplicates) |
625
+ | `core/src/types/blocks.ts` | FieldType with `'media-library'` |
626
+ | `core/migrations/021_media.sql` | Database migration |
627
+ | `apps/dev/app/api/v1/media/route.ts` | List + create endpoints |
628
+ | `apps/dev/app/api/v1/media/upload/route.ts` | File upload endpoint |
629
+ | `apps/dev/app/api/v1/media/[id]/route.ts` | Single item CRUD |
630
+ | `apps/dev/app/api/v1/media/[id]/tags/route.ts` | Media tag management |
631
+ | `apps/dev/app/api/v1/media-tags/route.ts` | All tags endpoint |
632
+ | `apps/dev/app/dashboard/(main)/media/page.tsx` | Dashboard page |
633
+
634
+ ## Anti-Patterns
635
+
636
+ ```typescript
637
+ // NEVER: Use type 'image' in block fields when you want the media library
638
+ {
639
+ name: 'backgroundImage',
640
+ type: 'image', // Opens basic file input
641
+ }
642
+
643
+ // CORRECT: Use type 'media-library' for full media library experience
644
+ {
645
+ name: 'backgroundImage',
646
+ type: 'media-library', // Opens MediaLibrary modal
647
+ }
648
+
649
+ // NEVER: Store media ID in block data (blocks store URLs)
650
+ onSelect={(media) => {
651
+ updateBlockData({ backgroundImage: media.id }) // WRONG
652
+ }}
653
+
654
+ // CORRECT: Store URL in block data
655
+ onSelect={(media) => {
656
+ updateBlockData({ backgroundImage: media.url }) // Correct
657
+ }}
658
+
659
+ // NEVER: Bypass MediaService for direct DB queries
660
+ const media = await queryWithRLS('SELECT * FROM "media" WHERE id = $1', [id], userId)
661
+
662
+ // CORRECT: Use MediaService
663
+ const media = await MediaService.getById(id, userId)
664
+
665
+ // NEVER: Skip duplicate check before upload
666
+ const result = await upload(file) // Might create duplicates
667
+
668
+ // CORRECT: Check for duplicates first (useMediaUpload handles this)
669
+ const { upload } = useMediaUpload() // Built-in duplicate detection
670
+ const result = await upload(file)
671
+
672
+ // NEVER: Hardcode file size limits
673
+ if (file.size > 10 * 1024 * 1024) { ... }
674
+
675
+ // CORRECT: Read from app.config.ts
676
+ import { appConfig } from '@/contents/themes/{theme}/config/app.config'
677
+ if (file.size > appConfig.media.maxSizeImageMB * 1024 * 1024) { ... }
678
+
679
+ // NEVER: Skip i18n for media UI strings
680
+ <Button>Upload</Button>
681
+
682
+ // CORRECT: Use translation keys
683
+ <Button>{t('upload')}</Button>
684
+
685
+ // NEVER: Forget data-cy selectors on interactive elements
686
+ <Button onClick={handleUpload}>Upload</Button>
687
+
688
+ // CORRECT: Include data-cy
689
+ <Button data-cy={sel(MEDIA_SELECTORS.library.uploadZone)} onClick={handleUpload}>
690
+ {t('upload')}
691
+ </Button>
692
+ ```
693
+
694
+ ## Checklist
695
+
696
+ ### Adding Media Selection to an Entity
697
+
698
+ - [ ] Import `MediaSelector` from `@/core/components/media`
699
+ - [ ] Add media field to entity form with proper `value` and `onChange`
700
+ - [ ] Add corresponding database column (TEXT for media ID or URL)
701
+ - [ ] Add i18n keys for the field label and description
702
+ - [ ] Add `data-cy` selector for the media field
703
+
704
+ ### Adding Media to a Block
705
+
706
+ - [ ] Use `type: 'media-library'` in field definition (not `type: 'image'`)
707
+ - [ ] Add `.url().optional()` Zod schema field for the URL
708
+ - [ ] Store URL string in block data (not media ID)
709
+ - [ ] Add i18n keys if custom labels needed
710
+ - [ ] Test in block editor: empty state, selection, change, remove
711
+
712
+ ### Creating Custom Media Workflow
713
+
714
+ - [ ] Use TanStack Query hooks from `useMedia.ts` (not raw fetch)
715
+ - [ ] Handle loading states with proper skeleton/spinner
716
+ - [ ] Handle error states with user-friendly messages
717
+ - [ ] Invalidate media queries after mutations
718
+ - [ ] Respect upload limits from `app.config.ts`
719
+ - [ ] Include duplicate detection via hash check
720
+ - [ ] All UI text uses `media` i18n namespace
721
+ - [ ] All interactive elements have `data-cy` selectors
722
+
723
+ ### API Integration
724
+
725
+ - [ ] Uses dual auth (session + API key)
726
+ - [ ] Validates input with Zod schemas
727
+ - [ ] Respects `media:read`, `media:write`, `media:delete` scopes
728
+ - [ ] Pagination parameters handled correctly
729
+ - [ ] File upload uses `FormData` (not JSON)
730
+ - [ ] Response follows standard `{ data, total, limit, offset }` format
731
+
732
+ ## Related Skills
733
+
734
+ - `page-builder-blocks` - Block field type `'media-library'` integration
735
+ - `entity-system` - MediaSelector for entity forms
736
+ - `tanstack-query` - useMedia hooks follow TanStack Query patterns
737
+ - `cypress-api` - Media API test patterns
738
+ - `how-to:handle-file-uploads` - Upload component and API guide
739
+ - `service-layer` - MediaService follows static class pattern with RLS
740
+ - `i18n-nextintl` - `media` namespace translations
741
+ - `cypress-selectors` - `MEDIA_SELECTORS` and `BLOCK_EDITOR_SELECTORS`
742
+ - `zod-validation` - Media schema validation patterns
743
+ - `database-migrations` - `021_media.sql` migration structure