@btst/stack 2.8.1 → 2.9.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 (214) hide show
  1. package/README.md +3 -2
  2. package/dist/components/markdown/index.d.cts +15 -2
  3. package/dist/components/markdown/index.d.mts +15 -2
  4. package/dist/components/markdown/index.d.ts +15 -2
  5. package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.cjs +30 -1
  6. package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.mjs +30 -1
  7. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.cjs +49 -9
  8. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.mjs +50 -10
  9. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.cjs +77 -9
  10. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.mjs +77 -9
  11. package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -5
  12. package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.mjs +24 -5
  13. package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.cjs +47 -13
  14. package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.mjs +47 -13
  15. package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.cjs +1 -1
  16. package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.mjs +1 -1
  17. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +6 -2
  18. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +6 -2
  19. package/dist/packages/stack/src/plugins/media/api/adapters/local.cjs +55 -0
  20. package/dist/packages/stack/src/plugins/media/api/adapters/local.mjs +37 -0
  21. package/dist/packages/stack/src/plugins/media/api/getters.cjs +83 -0
  22. package/dist/packages/stack/src/plugins/media/api/getters.mjs +78 -0
  23. package/dist/packages/stack/src/plugins/media/api/mutations.cjs +88 -0
  24. package/dist/packages/stack/src/plugins/media/api/mutations.mjs +82 -0
  25. package/dist/packages/stack/src/plugins/media/api/plugin.cjs +525 -0
  26. package/dist/packages/stack/src/plugins/media/api/plugin.mjs +523 -0
  27. package/dist/packages/stack/src/plugins/media/api/query-key-defs.cjs +19 -0
  28. package/dist/packages/stack/src/plugins/media/api/query-key-defs.mjs +16 -0
  29. package/dist/packages/stack/src/plugins/media/api/serializers.cjs +17 -0
  30. package/dist/packages/stack/src/plugins/media/api/serializers.mjs +14 -0
  31. package/dist/packages/stack/src/plugins/media/api/storage-adapter.cjs +15 -0
  32. package/dist/packages/stack/src/plugins/media/api/storage-adapter.mjs +11 -0
  33. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.cjs +129 -0
  34. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.mjs +127 -0
  35. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.cjs +58 -0
  36. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.mjs +56 -0
  37. package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.cjs +94 -0
  38. package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.mjs +92 -0
  39. package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.cjs +171 -0
  40. package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.mjs +168 -0
  41. package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.cjs +308 -0
  42. package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.mjs +305 -0
  43. package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.cjs +104 -0
  44. package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.mjs +102 -0
  45. package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.cjs +70 -0
  46. package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.mjs +68 -0
  47. package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.cjs +21 -0
  48. package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.mjs +17 -0
  49. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.cjs +35 -0
  50. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.cjs +125 -0
  51. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.mjs +123 -0
  52. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.mjs +33 -0
  53. package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +222 -0
  54. package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +214 -0
  55. package/dist/packages/stack/src/plugins/media/client/plugin.cjs +94 -0
  56. package/dist/packages/stack/src/plugins/media/client/plugin.mjs +92 -0
  57. package/dist/packages/stack/src/plugins/media/client/upload.cjs +121 -0
  58. package/dist/packages/stack/src/plugins/media/client/upload.mjs +119 -0
  59. package/dist/packages/stack/src/plugins/media/client/utils/image-compression.cjs +67 -0
  60. package/dist/packages/stack/src/plugins/media/client/utils/image-compression.mjs +65 -0
  61. package/dist/packages/stack/src/plugins/media/db.cjs +62 -0
  62. package/dist/packages/stack/src/plugins/media/db.mjs +60 -0
  63. package/dist/packages/stack/src/plugins/media/schemas.cjs +41 -0
  64. package/dist/packages/stack/src/plugins/media/schemas.mjs +35 -0
  65. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.cjs +18 -1
  66. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.mjs +19 -2
  67. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.cjs +2 -2
  68. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.mjs +2 -2
  69. package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.cjs +3 -2
  70. package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.mjs +3 -2
  71. package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.cjs +12 -5
  72. package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.mjs +12 -5
  73. package/dist/plugins/blog/api/index.d.cts +2 -2
  74. package/dist/plugins/blog/api/index.d.mts +2 -2
  75. package/dist/plugins/blog/api/index.d.ts +2 -2
  76. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  77. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  78. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  79. package/dist/plugins/blog/client/index.d.cts +60 -3
  80. package/dist/plugins/blog/client/index.d.mts +60 -3
  81. package/dist/plugins/blog/client/index.d.ts +60 -3
  82. package/dist/plugins/blog/query-keys.d.cts +2 -2
  83. package/dist/plugins/blog/query-keys.d.mts +2 -2
  84. package/dist/plugins/blog/query-keys.d.ts +2 -2
  85. package/dist/plugins/cms/client/index.d.cts +73 -3
  86. package/dist/plugins/cms/client/index.d.mts +73 -3
  87. package/dist/plugins/cms/client/index.d.ts +73 -3
  88. package/dist/plugins/kanban/api/index.d.cts +1 -1
  89. package/dist/plugins/kanban/api/index.d.mts +1 -1
  90. package/dist/plugins/kanban/api/index.d.ts +1 -1
  91. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  92. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  93. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  94. package/dist/plugins/kanban/client/index.d.cts +1 -1
  95. package/dist/plugins/kanban/client/index.d.mts +1 -1
  96. package/dist/plugins/kanban/client/index.d.ts +1 -1
  97. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  98. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  99. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  100. package/dist/plugins/media/api/adapters/s3.cjs +106 -0
  101. package/dist/plugins/media/api/adapters/s3.d.cts +60 -0
  102. package/dist/plugins/media/api/adapters/s3.d.mts +60 -0
  103. package/dist/plugins/media/api/adapters/s3.d.ts +60 -0
  104. package/dist/plugins/media/api/adapters/s3.mjs +104 -0
  105. package/dist/plugins/media/api/adapters/vercel-blob.cjs +54 -0
  106. package/dist/plugins/media/api/adapters/vercel-blob.d.cts +41 -0
  107. package/dist/plugins/media/api/adapters/vercel-blob.d.mts +41 -0
  108. package/dist/plugins/media/api/adapters/vercel-blob.d.ts +41 -0
  109. package/dist/plugins/media/api/adapters/vercel-blob.mjs +52 -0
  110. package/dist/plugins/media/api/index.cjs +26 -0
  111. package/dist/plugins/media/api/index.d.cts +116 -0
  112. package/dist/plugins/media/api/index.d.mts +116 -0
  113. package/dist/plugins/media/api/index.d.ts +116 -0
  114. package/dist/plugins/media/api/index.mjs +6 -0
  115. package/dist/plugins/media/client/components/index.cjs +10 -0
  116. package/dist/plugins/media/client/components/index.d.cts +55 -0
  117. package/dist/plugins/media/client/components/index.d.mts +55 -0
  118. package/dist/plugins/media/client/components/index.d.ts +55 -0
  119. package/dist/plugins/media/client/components/index.mjs +2 -0
  120. package/dist/plugins/media/client/hooks/index.cjs +13 -0
  121. package/dist/plugins/media/client/hooks/index.d.cts +53 -0
  122. package/dist/plugins/media/client/hooks/index.d.mts +53 -0
  123. package/dist/plugins/media/client/hooks/index.d.ts +53 -0
  124. package/dist/plugins/media/client/hooks/index.mjs +1 -0
  125. package/dist/plugins/media/client/index.cjs +9 -0
  126. package/dist/plugins/media/client/index.d.cts +242 -0
  127. package/dist/plugins/media/client/index.d.mts +242 -0
  128. package/dist/plugins/media/client/index.d.ts +242 -0
  129. package/dist/plugins/media/client/index.mjs +2 -0
  130. package/dist/plugins/media/client.css +1 -0
  131. package/dist/plugins/media/query-keys.cjs +72 -0
  132. package/dist/plugins/media/query-keys.d.cts +49 -0
  133. package/dist/plugins/media/query-keys.d.mts +49 -0
  134. package/dist/plugins/media/query-keys.d.ts +49 -0
  135. package/dist/plugins/media/query-keys.mjs +70 -0
  136. package/dist/plugins/media/style.css +1 -0
  137. package/dist/shared/{stack.DOZ1EXjM.d.mts → stack.6mEHS2WH.d.mts} +3 -3
  138. package/dist/shared/{stack.DX-tQ93o.d.cts → stack.AJTXI7kw.d.cts} +3 -3
  139. package/dist/shared/{stack.DRpeDS6X.d.ts → stack.BMx2QYOK.d.ts} +25 -0
  140. package/dist/shared/stack.BUTXWiG-.d.ts +286 -0
  141. package/dist/shared/stack.C7Y9sBDg.d.mts +286 -0
  142. package/dist/shared/stack.C7vfOBmO.d.mts +63 -0
  143. package/dist/shared/stack.CAni8dnD.d.cts +63 -0
  144. package/dist/shared/stack.CLcnSF_b.d.cts +25 -0
  145. package/dist/shared/stack.CLcnSF_b.d.mts +25 -0
  146. package/dist/shared/stack.CLcnSF_b.d.ts +25 -0
  147. package/dist/shared/stack.CYSwntXC.d.ts +63 -0
  148. package/dist/shared/{stack.Jb0kQDJC.d.mts → stack.Cd6McBu1.d.mts} +25 -0
  149. package/dist/shared/stack.CoBj86jf.d.cts +109 -0
  150. package/dist/shared/stack.CoBj86jf.d.mts +109 -0
  151. package/dist/shared/stack.CoBj86jf.d.ts +109 -0
  152. package/dist/shared/{stack.BXxrFL9R.d.ts → stack.D7HSzZdG.d.ts} +5 -5
  153. package/dist/shared/{stack.DzOhpIYM.d.mts → stack.DjgpFWq3.d.cts} +5 -5
  154. package/dist/shared/{stack.BxFl46lB.d.cts → stack.DxQl8Wa1.d.cts} +25 -0
  155. package/dist/shared/{stack.BSqJrCTM.d.cts → stack.IUeyQKrm.d.mts} +5 -5
  156. package/dist/shared/{stack.VF6FhyZw.d.ts → stack.QYn-Px94.d.ts} +3 -3
  157. package/dist/shared/stack.vxskCkim.d.cts +286 -0
  158. package/package.json +113 -4
  159. package/src/plugins/blog/client/components/forms/image-field.tsx +35 -4
  160. package/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +67 -12
  161. package/src/plugins/blog/client/components/forms/markdown-editor.tsx +106 -10
  162. package/src/plugins/blog/client/overrides.ts +58 -1
  163. package/src/plugins/cms/client/components/forms/content-form.tsx +26 -7
  164. package/src/plugins/cms/client/components/forms/file-upload.tsx +73 -15
  165. package/src/plugins/cms/client/overrides.ts +57 -2
  166. package/src/plugins/kanban/client/components/forms/board-form.tsx +1 -1
  167. package/src/plugins/kanban/client/components/forms/task-form.tsx +7 -1
  168. package/src/plugins/kanban/client/overrides.ts +25 -0
  169. package/src/plugins/media/__tests__/__stubs__/vercel-blob-server.ts +9 -0
  170. package/src/plugins/media/__tests__/getters.test.ts +274 -0
  171. package/src/plugins/media/__tests__/mutations.test.ts +299 -0
  172. package/src/plugins/media/__tests__/plugin.test.ts +752 -0
  173. package/src/plugins/media/__tests__/query-key-defs.test.ts +54 -0
  174. package/src/plugins/media/__tests__/storage-adapters.test.ts +351 -0
  175. package/src/plugins/media/api/adapters/local.ts +79 -0
  176. package/src/plugins/media/api/adapters/s3.ts +198 -0
  177. package/src/plugins/media/api/adapters/vercel-blob.ts +132 -0
  178. package/src/plugins/media/api/getters.ts +174 -0
  179. package/src/plugins/media/api/index.ts +41 -0
  180. package/src/plugins/media/api/mutations.ts +179 -0
  181. package/src/plugins/media/api/plugin.ts +855 -0
  182. package/src/plugins/media/api/query-key-defs.ts +41 -0
  183. package/src/plugins/media/api/serializers.ts +28 -0
  184. package/src/plugins/media/api/storage-adapter.ts +139 -0
  185. package/src/plugins/media/client/components/index.tsx +6 -0
  186. package/src/plugins/media/client/components/media-picker/asset-card.tsx +150 -0
  187. package/src/plugins/media/client/components/media-picker/asset-preview-button.tsx +67 -0
  188. package/src/plugins/media/client/components/media-picker/browse-tab.tsx +116 -0
  189. package/src/plugins/media/client/components/media-picker/folder-tree.tsx +188 -0
  190. package/src/plugins/media/client/components/media-picker/index.tsx +347 -0
  191. package/src/plugins/media/client/components/media-picker/upload-tab.tsx +108 -0
  192. package/src/plugins/media/client/components/media-picker/url-tab.tsx +72 -0
  193. package/src/plugins/media/client/components/media-picker/utils.ts +17 -0
  194. package/src/plugins/media/client/components/pages/library-page.internal.tsx +134 -0
  195. package/src/plugins/media/client/components/pages/library-page.tsx +42 -0
  196. package/src/plugins/media/client/hooks/index.tsx +9 -0
  197. package/src/plugins/media/client/hooks/use-media.tsx +289 -0
  198. package/src/plugins/media/client/index.ts +4 -0
  199. package/src/plugins/media/client/overrides.ts +127 -0
  200. package/src/plugins/media/client/plugin.tsx +184 -0
  201. package/src/plugins/media/client/upload.ts +171 -0
  202. package/src/plugins/media/client/utils/image-compression.ts +131 -0
  203. package/src/plugins/media/client.css +1 -0
  204. package/src/plugins/media/db.ts +62 -0
  205. package/src/plugins/media/query-keys.ts +96 -0
  206. package/src/plugins/media/schemas.ts +37 -0
  207. package/src/plugins/media/style.css +1 -0
  208. package/src/plugins/media/types.ts +26 -0
  209. package/dist/shared/{stack.BWp0hcm9.d.ts → stack.BQmuNl5p.d.cts} +3 -3
  210. package/dist/shared/{stack.BWp0hcm9.d.cts → stack.BQmuNl5p.d.mts} +3 -3
  211. package/dist/shared/{stack.BWp0hcm9.d.mts → stack.BQmuNl5p.d.ts} +3 -3
  212. package/dist/shared/{stack.BvCR4-9H.d.ts → stack.D4Cea8II.d.ts} +3 -3
  213. package/dist/shared/{stack.CWxAl9K3.d.mts → stack.HE_IvqV5.d.mts} +3 -3
  214. package/dist/shared/{stack.BOokfhZD.d.cts → stack.Rtcvl8sS.d.cts} +3 -3
@@ -0,0 +1,286 @@
1
+ import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
2
+ import { a as StorageAdapter, b as S3UploadToken } from './stack.CoBj86jf.cjs';
3
+ import { c as AssetListResult, l as listAssets, a as listFolders, A as AssetListParams } from './stack.CAni8dnD.cjs';
4
+ import * as better_call from 'better-call';
5
+ import { z } from 'zod';
6
+ import { A as Asset, F as Folder } from './stack.CLcnSF_b.cjs';
7
+
8
+ declare const AssetListQuerySchema: z.ZodObject<{
9
+ folderId: z.ZodOptional<z.ZodString>;
10
+ mimeType: z.ZodOptional<z.ZodString>;
11
+ query: z.ZodOptional<z.ZodString>;
12
+ offset: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
13
+ limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
14
+ }, z.core.$strip>;
15
+ declare const updateAssetSchema: z.ZodObject<{
16
+ alt: z.ZodOptional<z.ZodString>;
17
+ folderId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
18
+ }, z.core.$strip>;
19
+ declare const createFolderSchema: z.ZodObject<{
20
+ name: z.ZodString;
21
+ parentId: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strip>;
23
+
24
+ /**
25
+ * Context passed to media API hooks.
26
+ */
27
+ interface MediaApiContext<TBody = unknown, TParams = unknown, TQuery = unknown> {
28
+ body?: TBody;
29
+ params?: TParams;
30
+ query?: TQuery;
31
+ request?: Request;
32
+ headers?: Headers;
33
+ [key: string]: unknown;
34
+ }
35
+ /**
36
+ * Configuration hooks for the media backend plugin.
37
+ * All hooks are optional and allow consumers to customise behaviour.
38
+ */
39
+ interface MediaBackendHooks {
40
+ /**
41
+ * Called before a file upload is allowed (both direct and signed adapters).
42
+ * Throw an Error to reject the upload (e.g. if the user is not authenticated).
43
+ */
44
+ onBeforeUpload?: (meta: {
45
+ filename: string;
46
+ mimeType: string;
47
+ size?: number;
48
+ }, context: MediaApiContext) => Promise<void> | void;
49
+ /**
50
+ * Called after an asset record is created in the database.
51
+ */
52
+ onAfterUpload?: (asset: Asset, context: MediaApiContext) => Promise<void> | void;
53
+ /**
54
+ * Called before an asset is deleted. Throw to prevent deletion.
55
+ */
56
+ onBeforeDelete?: (asset: Asset, context: MediaApiContext) => Promise<void> | void;
57
+ /**
58
+ * Called after an asset has been deleted from the DB and storage.
59
+ */
60
+ onAfterDelete?: (assetId: string, context: MediaApiContext) => Promise<void> | void;
61
+ /**
62
+ * Called before listing assets. Throw to deny access.
63
+ */
64
+ onBeforeListAssets?: (filter: z.infer<typeof AssetListQuerySchema>, context: MediaApiContext) => Promise<void> | void;
65
+ /**
66
+ * Called before updating an asset (PATCH). Throw to deny access.
67
+ */
68
+ onBeforeUpdateAsset?: (asset: Asset, updates: z.infer<typeof updateAssetSchema>, context: MediaApiContext) => Promise<void> | void;
69
+ /**
70
+ * Called before listing folders. Throw to deny access.
71
+ */
72
+ onBeforeListFolders?: (filter: {
73
+ parentId?: string;
74
+ }, context: MediaApiContext) => Promise<void> | void;
75
+ /**
76
+ * Called before creating a folder. Throw to deny access.
77
+ */
78
+ onBeforeCreateFolder?: (input: z.infer<typeof createFolderSchema>, context: MediaApiContext) => Promise<void> | void;
79
+ /**
80
+ * Called before deleting a folder. Throw to deny access.
81
+ */
82
+ onBeforeDeleteFolder?: (folder: Folder, context: MediaApiContext) => Promise<void> | void;
83
+ }
84
+ /**
85
+ * Configuration for the media backend plugin.
86
+ */
87
+ interface MediaBackendConfig {
88
+ /**
89
+ * The storage adapter to use for file uploads.
90
+ * - `localAdapter()` — writes to the local filesystem (dev / self-hosted)
91
+ * - `s3Adapter()` — presigned PUT URL (AWS S3, Cloudflare R2, MinIO)
92
+ * - `vercelBlobAdapter()` — signed direct upload via Vercel Blob
93
+ */
94
+ storageAdapter: StorageAdapter;
95
+ /**
96
+ * Maximum file size in bytes.
97
+ * Enforced server-side for `localAdapter`.
98
+ * Passed into the Vercel Blob token for edge enforcement.
99
+ * Validated against the client-reported size for `s3Adapter`.
100
+ * @default 10485760 (10 MB)
101
+ */
102
+ maxFileSizeBytes?: number;
103
+ /**
104
+ * MIME type allowlist (e.g. `["image/jpeg", "image/png"]`).
105
+ * If omitted, all MIME types are accepted.
106
+ * Enforced server-side for `localAdapter`.
107
+ * Passed to Vercel Blob token for edge enforcement.
108
+ * Validated against the client-reported MIME type for `s3Adapter`.
109
+ */
110
+ allowedMimeTypes?: string[];
111
+ /**
112
+ * URL prefixes that are allowed when creating asset records via `POST /media/assets`.
113
+ * When omitted the plugin automatically derives a safe default from the storage adapter:
114
+ * - `s3Adapter` → the configured `publicBaseUrl`
115
+ * - `vercelBlobAdapter` → any URL whose hostname ends with `.public.blob.vercel-storage.com`
116
+ * - `localAdapter` → rejects client-supplied URLs; use `POST /media/upload` instead
117
+ *
118
+ * Provide this option only when you need to override the automatic default (e.g. to allow
119
+ * assets from a CDN in front of your storage that uses a different domain). When using
120
+ * `localAdapter`, setting `allowedUrlPrefixes` explicitly opts `POST /media/assets` back in.
121
+ */
122
+ allowedUrlPrefixes?: string[];
123
+ /**
124
+ * Optional lifecycle hooks for the media backend plugin.
125
+ */
126
+ hooks?: MediaBackendHooks;
127
+ }
128
+ /**
129
+ * Media backend plugin.
130
+ * Provides API endpoints for managing media assets and folders, and supports
131
+ * local, S3-compatible, and Vercel Blob storage backends.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * import { mediaBackendPlugin, localAdapter } from "@btst/stack/plugins/media/api";
136
+ *
137
+ * mediaBackendPlugin({
138
+ * storageAdapter: localAdapter(),
139
+ * hooks: {
140
+ * onBeforeUpload: async (_meta, ctx) => {
141
+ * const session = await getSession(ctx.headers as Headers);
142
+ * if (!session) throw new Error("Unauthorized");
143
+ * },
144
+ * },
145
+ * })
146
+ * ```
147
+ */
148
+ declare const mediaBackendPlugin: (config: MediaBackendConfig) => _btst_stack_plugins_api.BackendPlugin<{
149
+ readonly listAssets: better_call.StrictEndpoint<"/media/assets", {} & {
150
+ method: "GET";
151
+ } & {
152
+ query: better_call.StandardSchemaV1<{
153
+ folderId?: string | undefined;
154
+ mimeType?: string | undefined;
155
+ query?: string | undefined;
156
+ offset?: unknown;
157
+ limit?: unknown;
158
+ }, {
159
+ folderId?: string | undefined;
160
+ mimeType?: string | undefined;
161
+ query?: string | undefined;
162
+ offset?: unknown;
163
+ limit?: unknown;
164
+ }>;
165
+ }, AssetListResult>;
166
+ readonly createAsset: better_call.StrictEndpoint<"/media/assets", {} & {
167
+ method: "POST";
168
+ body: better_call.StandardSchemaV1<{
169
+ filename: string;
170
+ originalName: string;
171
+ mimeType: string;
172
+ size: number;
173
+ url: string;
174
+ folderId?: string | undefined;
175
+ alt?: string | undefined;
176
+ }, {
177
+ filename: string;
178
+ originalName: string;
179
+ mimeType: string;
180
+ size: number;
181
+ url: string;
182
+ folderId?: string | undefined;
183
+ alt?: string | undefined;
184
+ }>;
185
+ }, Asset>;
186
+ readonly updateAsset: better_call.StrictEndpoint<"/media/assets/:id", {} & {
187
+ method: "PATCH";
188
+ body: better_call.StandardSchemaV1<{
189
+ alt?: string | undefined;
190
+ folderId?: string | null | undefined;
191
+ }, {
192
+ alt?: string | undefined;
193
+ folderId?: string | null | undefined;
194
+ }>;
195
+ }, Asset>;
196
+ readonly deleteAsset: better_call.StrictEndpoint<"/media/assets/:id", {} & {
197
+ method: "DELETE";
198
+ body: better_call.StandardSchemaV1<unknown, unknown>;
199
+ }, {
200
+ success: boolean;
201
+ }>;
202
+ readonly listFolders: better_call.StrictEndpoint<"/media/folders", {} & {
203
+ method: "GET";
204
+ } & {
205
+ query: better_call.StandardSchemaV1<{
206
+ parentId?: string | undefined;
207
+ }, {
208
+ parentId?: string | undefined;
209
+ }>;
210
+ }, Folder[]>;
211
+ readonly createFolder: better_call.StrictEndpoint<"/media/folders", {} & {
212
+ method: "POST";
213
+ body: better_call.StandardSchemaV1<{
214
+ name: string;
215
+ parentId?: string | undefined;
216
+ }, {
217
+ name: string;
218
+ parentId?: string | undefined;
219
+ }>;
220
+ }, Folder>;
221
+ readonly deleteFolder: better_call.StrictEndpoint<"/media/folders/:id", {} & {
222
+ method: "DELETE";
223
+ body: better_call.StandardSchemaV1<unknown, unknown>;
224
+ }, {
225
+ success: boolean;
226
+ }>;
227
+ readonly uploadDirect: better_call.StrictEndpoint<"/media/upload", {
228
+ metadata: {
229
+ allowedMediaTypes: string[];
230
+ };
231
+ } & {
232
+ method: "POST";
233
+ body: better_call.StandardSchemaV1<unknown, unknown>;
234
+ }, Asset>;
235
+ readonly uploadToken: better_call.StrictEndpoint<"/media/upload/token", {} & {
236
+ method: "POST";
237
+ body: better_call.StandardSchemaV1<{
238
+ filename: string;
239
+ mimeType: string;
240
+ size: number;
241
+ folderId?: string | undefined;
242
+ }, {
243
+ filename: string;
244
+ mimeType: string;
245
+ size: number;
246
+ folderId?: string | undefined;
247
+ }>;
248
+ }, S3UploadToken>;
249
+ readonly uploadVercelBlob: better_call.StrictEndpoint<"/media/upload/vercel-blob", {} & {
250
+ method: "POST";
251
+ body: better_call.StandardSchemaV1<unknown, unknown>;
252
+ }, unknown>;
253
+ }, {
254
+ listAssets: (params?: Parameters<typeof listAssets>[1]) => Promise<AssetListResult>;
255
+ getAssetById: (id: string) => Promise<Asset | null>;
256
+ listFolders: (params?: Parameters<typeof listFolders>[1]) => Promise<Folder[]>;
257
+ getFolderById: (id: string) => Promise<Folder | null>;
258
+ }>;
259
+ type MediaApiRouter = ReturnType<ReturnType<typeof mediaBackendPlugin>["routes"]>;
260
+
261
+ /**
262
+ * Internal query key constants for the media plugin.
263
+ * Shared between query-keys.ts (HTTP path) and any SSR/SSG prefetching
264
+ * to prevent key drift between client and server.
265
+ */
266
+
267
+ /**
268
+ * Discriminator for the asset list cache key.
269
+ */
270
+ interface AssetListDiscriminator {
271
+ folderId: string | undefined;
272
+ mimeType: string | undefined;
273
+ query: string | undefined;
274
+ limit: number | undefined;
275
+ offset: number | undefined;
276
+ }
277
+ declare function assetListDiscriminator(params?: AssetListParams): AssetListDiscriminator;
278
+ /** Full query key builders — use these with `queryClient.setQueryData()`. */
279
+ declare const MEDIA_QUERY_KEYS: {
280
+ assetsList: (params?: AssetListParams) => readonly ["media", "assets", "list", AssetListDiscriminator];
281
+ assetDetail: (id: string) => readonly ["media", "assets", "detail", string];
282
+ foldersList: (parentId?: string | null) => readonly ["media", "folders", "list", string];
283
+ };
284
+
285
+ export { MEDIA_QUERY_KEYS as a, assetListDiscriminator as b, mediaBackendPlugin as m };
286
+ export type { AssetListDiscriminator as A, MediaApiRouter as M, MediaApiContext as c, MediaBackendHooks as d, MediaBackendConfig as e };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -414,6 +414,77 @@
414
414
  }
415
415
  },
416
416
  "./plugins/comments/css": "./dist/plugins/comments/style.css",
417
+ "./plugins/media/api": {
418
+ "import": {
419
+ "types": "./dist/plugins/media/api/index.d.ts",
420
+ "default": "./dist/plugins/media/api/index.mjs"
421
+ },
422
+ "require": {
423
+ "types": "./dist/plugins/media/api/index.d.cts",
424
+ "default": "./dist/plugins/media/api/index.cjs"
425
+ }
426
+ },
427
+ "./plugins/media/api/adapters/s3": {
428
+ "import": {
429
+ "types": "./dist/plugins/media/api/adapters/s3.d.ts",
430
+ "default": "./dist/plugins/media/api/adapters/s3.mjs"
431
+ },
432
+ "require": {
433
+ "types": "./dist/plugins/media/api/adapters/s3.d.cts",
434
+ "default": "./dist/plugins/media/api/adapters/s3.cjs"
435
+ }
436
+ },
437
+ "./plugins/media/api/adapters/vercel-blob": {
438
+ "import": {
439
+ "types": "./dist/plugins/media/api/adapters/vercel-blob.d.ts",
440
+ "default": "./dist/plugins/media/api/adapters/vercel-blob.mjs"
441
+ },
442
+ "require": {
443
+ "types": "./dist/plugins/media/api/adapters/vercel-blob.d.cts",
444
+ "default": "./dist/plugins/media/api/adapters/vercel-blob.cjs"
445
+ }
446
+ },
447
+ "./plugins/media/client": {
448
+ "import": {
449
+ "types": "./dist/plugins/media/client/index.d.ts",
450
+ "default": "./dist/plugins/media/client/index.mjs"
451
+ },
452
+ "require": {
453
+ "types": "./dist/plugins/media/client/index.d.cts",
454
+ "default": "./dist/plugins/media/client/index.cjs"
455
+ }
456
+ },
457
+ "./plugins/media/client/components": {
458
+ "import": {
459
+ "types": "./dist/plugins/media/client/components/index.d.ts",
460
+ "default": "./dist/plugins/media/client/components/index.mjs"
461
+ },
462
+ "require": {
463
+ "types": "./dist/plugins/media/client/components/index.d.cts",
464
+ "default": "./dist/plugins/media/client/components/index.cjs"
465
+ }
466
+ },
467
+ "./plugins/media/client/hooks": {
468
+ "import": {
469
+ "types": "./dist/plugins/media/client/hooks/index.d.ts",
470
+ "default": "./dist/plugins/media/client/hooks/index.mjs"
471
+ },
472
+ "require": {
473
+ "types": "./dist/plugins/media/client/hooks/index.d.cts",
474
+ "default": "./dist/plugins/media/client/hooks/index.cjs"
475
+ }
476
+ },
477
+ "./plugins/media/query-keys": {
478
+ "import": {
479
+ "types": "./dist/plugins/media/query-keys.d.ts",
480
+ "default": "./dist/plugins/media/query-keys.mjs"
481
+ },
482
+ "require": {
483
+ "types": "./dist/plugins/media/query-keys.d.cts",
484
+ "default": "./dist/plugins/media/query-keys.cjs"
485
+ }
486
+ },
487
+ "./plugins/media/css": "./dist/plugins/media/client.css",
417
488
  "./plugins/route-docs/client": {
418
489
  "import": {
419
490
  "types": "./dist/plugins/route-docs/client/index.d.ts",
@@ -610,6 +681,27 @@
610
681
  "plugins/comments/query-keys": [
611
682
  "./dist/plugins/comments/query-keys.d.ts"
612
683
  ],
684
+ "plugins/media/api": [
685
+ "./dist/plugins/media/api/index.d.ts"
686
+ ],
687
+ "plugins/media/api/adapters/s3": [
688
+ "./dist/plugins/media/api/adapters/s3.d.ts"
689
+ ],
690
+ "plugins/media/api/adapters/vercel-blob": [
691
+ "./dist/plugins/media/api/adapters/vercel-blob.d.ts"
692
+ ],
693
+ "plugins/media/client": [
694
+ "./dist/plugins/media/client/index.d.ts"
695
+ ],
696
+ "plugins/media/client/components": [
697
+ "./dist/plugins/media/client/components/index.d.ts"
698
+ ],
699
+ "plugins/media/client/hooks": [
700
+ "./dist/plugins/media/client/hooks/index.d.ts"
701
+ ],
702
+ "plugins/media/query-keys": [
703
+ "./dist/plugins/media/query-keys.d.ts"
704
+ ],
613
705
  "plugins/route-docs/client": [
614
706
  "./dist/plugins/route-docs/client/index.d.ts"
615
707
  ],
@@ -646,13 +738,17 @@
646
738
  },
647
739
  "peerDependencies": {
648
740
  "@ai-sdk/react": ">=2.0.0",
741
+ "@aws-sdk/client-s3": ">=3.0.0",
742
+ "@aws-sdk/s3-request-presigner": ">=3.0.0",
649
743
  "@btst/yar": ">=1.2.0",
650
744
  "@hookform/resolvers": ">=5.0.0",
651
745
  "@radix-ui/react-dialog": ">=1.1.0",
652
746
  "@radix-ui/react-label": ">=2.1.0",
653
747
  "@radix-ui/react-slot": ">=1.1.0",
654
748
  "@radix-ui/react-switch": ">=1.1.0",
749
+ "@tailwindcss/typography": ">=0.5.0",
655
750
  "@tanstack/react-query": "^5.0.0",
751
+ "@vercel/blob": ">=0.14.0",
656
752
  "ai": ">=5.0.0",
657
753
  "better-call": ">=1.3.2",
658
754
  "class-variance-authority": ">=0.7.0",
@@ -675,25 +771,38 @@
675
771
  "sonner": ">=2.0.0",
676
772
  "tailwind-merge": ">=2.6.0",
677
773
  "tailwindcss": ">=3.0.0",
678
- "@tailwindcss/typography": ">=0.5.0",
679
774
  "zod": ">=4.2.0"
680
775
  },
776
+ "peerDependenciesMeta": {
777
+ "@vercel/blob": {
778
+ "optional": true
779
+ },
780
+ "@aws-sdk/client-s3": {
781
+ "optional": true
782
+ },
783
+ "@aws-sdk/s3-request-presigner": {
784
+ "optional": true
785
+ }
786
+ },
681
787
  "devDependencies": {
682
- "tsx": "catalog:",
683
788
  "@ai-sdk/react": "^2.0.94",
789
+ "@aws-sdk/client-s3": "^3.1011.0",
790
+ "@aws-sdk/s3-request-presigner": "^3.1011.0",
684
791
  "@btst/adapter-memory": "2.1.1",
685
792
  "@btst/yar": "1.2.0",
686
793
  "@types/react": "^19.0.0",
687
794
  "@types/slug": "^5.0.9",
795
+ "@vercel/blob": "^0.27.3",
688
796
  "@workspace/ui": "workspace:*",
689
797
  "ai": "^5.0.94",
690
798
  "better-call": "catalog:",
799
+ "knip": "^5.61.2",
691
800
  "react": "^19.1.1",
692
801
  "react-dom": "^19.1.1",
693
802
  "react-error-boundary": "^4.1.2",
694
- "knip": "^5.61.2",
695
803
  "rollup-plugin-preserve-directives": "0.4.0",
696
804
  "rollup-plugin-visualizer": "^5.12.0",
805
+ "tsx": "catalog:",
697
806
  "typescript": "catalog:",
698
807
  "unbuild": "catalog:",
699
808
  "vitest": "catalog:",
@@ -28,13 +28,44 @@ export function FeaturedImageField({
28
28
  const fileInputRef = useRef<HTMLInputElement>(null);
29
29
  const [isUploading, setIsUploading] = useState(false);
30
30
 
31
- const { uploadImage, Image, localization } = usePluginOverrides<
32
- BlogPluginOverrides,
33
- Partial<BlogPluginOverrides>
34
- >("blog", { localization: BLOG_LOCALIZATION });
31
+ const {
32
+ uploadImage,
33
+ Image,
34
+ localization,
35
+ imageInputField: ImageInput,
36
+ } = usePluginOverrides<BlogPluginOverrides, Partial<BlogPluginOverrides>>(
37
+ "blog",
38
+ { localization: BLOG_LOCALIZATION },
39
+ );
35
40
 
36
41
  const ImageComponent = Image ? Image : DefaultImage;
37
42
 
43
+ // When a custom imageInput component is provided via overrides, delegate to it.
44
+ if (ImageInput) {
45
+ return (
46
+ <FormItem className="flex flex-col">
47
+ <FormLabel>
48
+ {localization.BLOG_FORMS_FEATURED_IMAGE_LABEL}
49
+ {isRequired && (
50
+ <span className="text-destructive">
51
+ {" "}
52
+ {localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK}
53
+ </span>
54
+ )}
55
+ </FormLabel>
56
+ <FormControl>
57
+ <ImageInput
58
+ value={value || ""}
59
+ onChange={onChange}
60
+ isRequired={isRequired}
61
+ />
62
+ </FormControl>
63
+ <FormDescription />
64
+ <FormMessage />
65
+ </FormItem>
66
+ );
67
+ }
68
+
38
69
  const handleImageUpload = async (
39
70
  event: React.ChangeEvent<HTMLInputElement>,
40
71
  ) => {
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { useCallback, useRef } from "react";
2
3
  import { usePluginOverrides } from "@btst/stack/context";
3
4
  import type { BlogPluginOverrides } from "../../overrides";
4
5
  import { BLOG_LOCALIZATION } from "../../localization";
@@ -6,24 +7,78 @@ import { MarkdownEditor, type MarkdownEditorProps } from "./markdown-editor";
6
7
 
7
8
  type MarkdownEditorWithOverridesProps = Omit<
8
9
  MarkdownEditorProps,
9
- "uploadImage" | "placeholder"
10
+ | "uploadImage"
11
+ | "placeholder"
12
+ | "insertImageRef"
13
+ | "openMediaPickerForImageBlock"
10
14
  >;
11
15
 
12
16
  export function MarkdownEditorWithOverrides(
13
17
  props: MarkdownEditorWithOverridesProps,
14
18
  ) {
15
- const { uploadImage, localization } = usePluginOverrides<
16
- BlogPluginOverrides,
17
- Partial<BlogPluginOverrides>
18
- >("blog", {
19
- localization: BLOG_LOCALIZATION,
20
- });
19
+ const {
20
+ uploadImage,
21
+ imagePicker: ImagePickerTrigger,
22
+ localization,
23
+ } = usePluginOverrides<BlogPluginOverrides, Partial<BlogPluginOverrides>>(
24
+ "blog",
25
+ { localization: BLOG_LOCALIZATION },
26
+ );
27
+
28
+ const insertImageRef = useRef<((url: string) => void) | null>(null);
29
+ // Holds the Crepe-image-block `setUrl` callback while the picker is open.
30
+ const pendingInsertUrlRef = useRef<((url: string) => void) | null>(null);
31
+ // Ref to the trigger wrapper so we can programmatically click the picker button.
32
+ const triggerContainerRef = useRef<HTMLDivElement | null>(null);
33
+
34
+ // Single onSelect handler for ImagePickerTrigger.
35
+ // URLs returned by the media plugin are already percent-encoded at the
36
+ // source (storage adapter), so no additional encoding is applied here.
37
+ const handleSelect = useCallback((url: string) => {
38
+ if (pendingInsertUrlRef.current) {
39
+ // Crepe image block flow: set the URL into the block's link input.
40
+ pendingInsertUrlRef.current(url);
41
+ pendingInsertUrlRef.current = null;
42
+ } else {
43
+ // Normal flow: insert image at end of markdown content.
44
+ insertImageRef.current?.(url);
45
+ }
46
+ }, []);
47
+
48
+ // Called by MarkdownEditor's click interceptor when the user clicks a Crepe
49
+ // image-block upload placeholder.
50
+ const openMediaPickerForImageBlock = useCallback(
51
+ (setUrl: (url: string) => void) => {
52
+ pendingInsertUrlRef.current = setUrl;
53
+ // Programmatically click the visible picker trigger button.
54
+ const btn = triggerContainerRef.current?.querySelector(
55
+ '[data-testid="open-media-picker"]',
56
+ ) as HTMLButtonElement | null;
57
+ btn?.click();
58
+ },
59
+ [],
60
+ );
21
61
 
22
62
  return (
23
- <MarkdownEditor
24
- {...props}
25
- uploadImage={uploadImage}
26
- placeholder={localization?.BLOG_FORMS_EDITOR_PLACEHOLDER}
27
- />
63
+ <div className="flex flex-col">
64
+ <MarkdownEditor
65
+ {...props}
66
+ uploadImage={uploadImage}
67
+ placeholder={localization?.BLOG_FORMS_EDITOR_PLACEHOLDER}
68
+ insertImageRef={insertImageRef}
69
+ openMediaPickerForImageBlock={
70
+ ImagePickerTrigger ? openMediaPickerForImageBlock : undefined
71
+ }
72
+ />
73
+ {ImagePickerTrigger && (
74
+ <div
75
+ ref={triggerContainerRef}
76
+ className="flex justify-end mt-1"
77
+ data-testid="image-picker-trigger"
78
+ >
79
+ <ImagePickerTrigger onSelect={handleSelect} />
80
+ </div>
81
+ )}
82
+ </div>
28
83
  );
29
84
  }