opacacms 0.1.11 → 0.1.13

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 (246) hide show
  1. package/dist/admin/index.js +9464 -21
  2. package/dist/admin/webcomponent.d.ts +1 -1
  3. package/dist/admin/webcomponent.js +9620 -6
  4. package/dist/admin.css +1 -0
  5. package/dist/{chunk-6dhs73zq.js → chunk-0am1m47g.js} +1 -1
  6. package/dist/{chunk-0nf7fe26.js → chunk-0d9aqz6z.js} +1 -1
  7. package/dist/{chunk-cvdd4eqh.js → chunk-2kyhqvhc.js} +5 -1
  8. package/dist/{chunk-gjjcc4hm.js → chunk-2z8wxx9g.js} +21 -6
  9. package/dist/{chunk-xg35h5a3.js → chunk-7fyepksb.js} +1 -1
  10. package/dist/{chunk-njytmdb4.js → chunk-pxh5encs.js} +34 -24
  11. package/dist/{chunk-n8aekdnr.js → chunk-qkn1ykrj.js} +33 -23
  12. package/dist/{chunk-kwp83w8b.js → chunk-wmvjvn7b.js} +4 -4
  13. package/dist/{chunk-qrt22f6e.js → chunk-wq314kkx.js} +35 -25
  14. package/dist/{chunk-eqtsfyjf.js → chunk-x2ejaftz.js} +52 -28
  15. package/dist/{chunk-6ew02s0c.js → chunk-xtwc125q.js} +18 -18
  16. package/dist/cli/index.js +5 -5
  17. package/dist/db/better-sqlite.d.ts +1 -0
  18. package/dist/db/better-sqlite.js +3 -3
  19. package/dist/db/bun-sqlite.d.ts +1 -0
  20. package/dist/db/bun-sqlite.js +3 -3
  21. package/dist/db/d1.js +3 -3
  22. package/dist/db/index.d.ts +3 -0
  23. package/dist/db/index.js +17 -13
  24. package/dist/db/postgres.js +3 -3
  25. package/dist/db/sqlite.js +3 -3
  26. package/dist/runtimes/bun.js +2 -2
  27. package/dist/runtimes/cloudflare-workers.js +2 -2
  28. package/dist/runtimes/next.js +2 -2
  29. package/dist/runtimes/node.js +2 -2
  30. package/dist/server.js +2 -2
  31. package/package.json +8 -2
  32. package/bun.lock +0 -34
  33. package/dist/admin/index.css +0 -47
  34. package/dist/api.d.ts +0 -6
  35. package/dist/api.js +0 -27
  36. package/dist/chunk-2zm8cy1w.js +0 -9482
  37. package/global.d.ts +0 -11
  38. package/src/admin/api-client.ts +0 -63
  39. package/src/admin/auth-client.ts +0 -40
  40. package/src/admin/custom-field.ts +0 -179
  41. package/src/admin/index.ts +0 -15
  42. package/src/admin/react.tsx +0 -72
  43. package/src/admin/router.ts +0 -9
  44. package/src/admin/stores/admin-queries.ts +0 -121
  45. package/src/admin/stores/auth.ts +0 -61
  46. package/src/admin/stores/column-visibility.ts +0 -67
  47. package/src/admin/stores/config.ts +0 -15
  48. package/src/admin/stores/media.ts +0 -95
  49. package/src/admin/stores/query.ts +0 -13
  50. package/src/admin/stores/ui.ts +0 -29
  51. package/src/admin/ui/admin-client.tsx +0 -283
  52. package/src/admin/ui/admin-layout.tsx +0 -276
  53. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  54. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  55. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  56. package/src/admin/ui/components/Table.tsx +0 -67
  57. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  58. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  59. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  60. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  61. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  62. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  63. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  64. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  65. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  66. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  67. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  68. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  69. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  70. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  71. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  72. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  73. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  74. package/src/admin/ui/components/fields/index.tsx +0 -371
  75. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  76. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  77. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  78. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  79. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  80. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  81. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  82. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  83. package/src/admin/ui/components/fields/utils.ts +0 -1
  84. package/src/admin/ui/components/link.tsx +0 -41
  85. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  86. package/src/admin/ui/components/toast.tsx +0 -72
  87. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  88. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  89. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  90. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  91. package/src/admin/ui/components/ui/button.tsx +0 -26
  92. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  93. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  94. package/src/admin/ui/components/ui/group.tsx +0 -20
  95. package/src/admin/ui/components/ui/index.ts +0 -17
  96. package/src/admin/ui/components/ui/input.tsx +0 -12
  97. package/src/admin/ui/components/ui/join.tsx +0 -53
  98. package/src/admin/ui/components/ui/label.tsx +0 -11
  99. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  100. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  101. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  102. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  103. package/src/admin/ui/components/ui/select.tsx +0 -187
  104. package/src/admin/ui/components/ui/separator.tsx +0 -21
  105. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  106. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  107. package/src/admin/ui/components/ui/utils.ts +0 -3
  108. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  109. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  110. package/src/admin/ui/styles/accordion.scss +0 -60
  111. package/src/admin/ui/styles/animations.scss +0 -41
  112. package/src/admin/ui/styles/asset-manager.scss +0 -547
  113. package/src/admin/ui/styles/badge.scss +0 -13
  114. package/src/admin/ui/styles/base.scss +0 -22
  115. package/src/admin/ui/styles/button.scss +0 -161
  116. package/src/admin/ui/styles/card.scss +0 -13
  117. package/src/admin/ui/styles/collapsible.scss +0 -75
  118. package/src/admin/ui/styles/data-detail.scss +0 -92
  119. package/src/admin/ui/styles/dialog.scss +0 -102
  120. package/src/admin/ui/styles/empty-state.scss +0 -22
  121. package/src/admin/ui/styles/group.scss +0 -19
  122. package/src/admin/ui/styles/index.scss +0 -33
  123. package/src/admin/ui/styles/input.scss +0 -80
  124. package/src/admin/ui/styles/label.scss +0 -12
  125. package/src/admin/ui/styles/layout.scss +0 -56
  126. package/src/admin/ui/styles/lexical.scss +0 -469
  127. package/src/admin/ui/styles/loading.scss +0 -102
  128. package/src/admin/ui/styles/media-registry.scss +0 -597
  129. package/src/admin/ui/styles/pagination.scss +0 -20
  130. package/src/admin/ui/styles/radio-group.scss +0 -66
  131. package/src/admin/ui/styles/row.scss +0 -17
  132. package/src/admin/ui/styles/scrollbar.scss +0 -36
  133. package/src/admin/ui/styles/select.scss +0 -121
  134. package/src/admin/ui/styles/separator.scss +0 -14
  135. package/src/admin/ui/styles/sheet.scss +0 -152
  136. package/src/admin/ui/styles/sidebar.scss +0 -148
  137. package/src/admin/ui/styles/switch.scss +0 -59
  138. package/src/admin/ui/styles/table.scss +0 -207
  139. package/src/admin/ui/styles/tabs.scss +0 -62
  140. package/src/admin/ui/styles/toast.scss +0 -45
  141. package/src/admin/ui/styles/variables.scss +0 -24
  142. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  143. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  144. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  145. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  146. package/src/admin/ui/views/init-view.tsx +0 -182
  147. package/src/admin/ui/views/login-view.tsx +0 -123
  148. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  149. package/src/admin/ui/views/settings-view.tsx +0 -729
  150. package/src/admin/webcomponent.tsx +0 -15
  151. package/src/api.ts +0 -9
  152. package/src/auth/index.ts +0 -194
  153. package/src/auth/migrations.ts +0 -87
  154. package/src/auth/premissions.ts +0 -46
  155. package/src/cli/commands/generate-types.ts +0 -116
  156. package/src/cli/commands/init.ts +0 -95
  157. package/src/cli/commands/migrate-commands.ts +0 -160
  158. package/src/cli/commands/seed-command.ts +0 -11
  159. package/src/cli/d1-mock.ts +0 -101
  160. package/src/cli/index.test.ts +0 -84
  161. package/src/cli/index.ts +0 -183
  162. package/src/cli/r2-mock.ts +0 -217
  163. package/src/cli/seeding.ts +0 -409
  164. package/src/client.ts +0 -181
  165. package/src/config-utils.ts +0 -102
  166. package/src/config.ts +0 -49
  167. package/src/db/adapter.ts +0 -53
  168. package/src/db/better-sqlite.ts +0 -632
  169. package/src/db/bun-sqlite.ts +0 -646
  170. package/src/db/d1.ts +0 -711
  171. package/src/db/index.ts +0 -6
  172. package/src/db/kysely/data-mapper.ts +0 -142
  173. package/src/db/kysely/field-mapper.ts +0 -148
  174. package/src/db/kysely/migration-generator.ts +0 -223
  175. package/src/db/kysely/query-builder.ts +0 -92
  176. package/src/db/kysely/schema-builder.ts +0 -439
  177. package/src/db/kysely/sql-utils.ts +0 -13
  178. package/src/db/postgres.ts +0 -621
  179. package/src/db/sqlite.ts +0 -660
  180. package/src/db/system-schema.ts +0 -121
  181. package/src/index.ts +0 -13
  182. package/src/runtimes/README.md +0 -59
  183. package/src/runtimes/bun.ts +0 -49
  184. package/src/runtimes/cloudflare-workers.ts +0 -38
  185. package/src/runtimes/next.ts +0 -26
  186. package/src/runtimes/node.ts +0 -52
  187. package/src/schema/collection.ts +0 -184
  188. package/src/schema/fields/base.ts +0 -164
  189. package/src/schema/fields/index.ts +0 -427
  190. package/src/schema/global.ts +0 -145
  191. package/src/schema/index.ts +0 -4
  192. package/src/schema/infer.ts +0 -72
  193. package/src/server/admin-router.ts +0 -20
  194. package/src/server/admin.ts +0 -142
  195. package/src/server/assets.ts +0 -306
  196. package/src/server/collection-router.ts +0 -55
  197. package/src/server/handlers.ts +0 -722
  198. package/src/server/middlewares/admin.ts +0 -27
  199. package/src/server/middlewares/auth.ts +0 -89
  200. package/src/server/middlewares/context.ts +0 -17
  201. package/src/server/middlewares/cors.ts +0 -24
  202. package/src/server/middlewares/database-init.ts +0 -74
  203. package/src/server/middlewares/rate-limit.ts +0 -77
  204. package/src/server/router.ts +0 -47
  205. package/src/server/setup-middlewares.ts +0 -58
  206. package/src/server/system-router.ts +0 -35
  207. package/src/server.ts +0 -9
  208. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  209. package/src/storage/adapters/local.ts +0 -146
  210. package/src/storage/adapters/s3.ts +0 -186
  211. package/src/storage/errors.ts +0 -46
  212. package/src/storage/index.ts +0 -5
  213. package/src/storage/types.ts +0 -39
  214. package/src/types.ts +0 -577
  215. package/src/utils/lexical.ts +0 -37
  216. package/src/utils/logger.ts +0 -73
  217. package/src/validation.ts +0 -429
  218. package/src/validator.ts +0 -179
  219. package/test/admin-custom-field.test.ts +0 -162
  220. package/test/admin-react-field.test.tsx +0 -134
  221. package/test/api-features.test.ts +0 -78
  222. package/test/api.test.ts +0 -178
  223. package/test/auth.test.ts +0 -62
  224. package/test/cli-integration.test.ts +0 -148
  225. package/test/cli.test.ts +0 -25
  226. package/test/db/postgres.test.ts +0 -95
  227. package/test/db/sqlite-filter.test.ts +0 -53
  228. package/test/db/sqlite.test.ts +0 -82
  229. package/test/engine-features.test.ts +0 -79
  230. package/test/globals.test.ts +0 -74
  231. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  232. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  233. package/test/integration-tmp/my-test-app/index.ts +0 -8
  234. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  235. package/test/integration-tmp/my-test-app/package.json +0 -12
  236. package/test/populate.test.ts +0 -79
  237. package/test/runtimes.test.ts +0 -43
  238. package/test/schema-builder.test.ts +0 -107
  239. package/test/schema-features.test.ts +0 -63
  240. package/test/seeding.test.ts +0 -68
  241. package/test/storage/local.test.ts +0 -72
  242. package/test/storage/s3.test.ts +0 -60
  243. package/test/structural-data.test.ts +0 -100
  244. package/test/test-setup.ts +0 -11
  245. package/test/validation.test.ts +0 -162
  246. package/tsconfig.json +0 -42
@@ -1,334 +0,0 @@
1
- import { useStore } from '@nanostores/react';
2
- import {
3
- ChevronRight,
4
- File,
5
- FileText,
6
- FolderPlus,
7
- Image as ImageIcon,
8
- Loader2,
9
- Upload,
10
- X,
11
- } from 'lucide-react';
12
- import React, { useMemo, useRef, useState } from 'react';
13
- import { api, getCurrentBaseURL } from '../../../api-client';
14
- import { $config } from '../../../stores/config';
15
- import {
16
- $assets,
17
- $mediaCurrentFolder,
18
- $mediaSelectedBucket,
19
- $mediaViewMode,
20
- type AssetDoc,
21
- setMediaBucket,
22
- setMediaFolder,
23
- } from '../../../stores/media';
24
- import '../../styles/asset-manager.scss';
25
- import {
26
- Select,
27
- SelectContent,
28
- SelectItem,
29
- SelectLabel,
30
- SelectSeparator,
31
- SelectTrigger,
32
- SelectValue,
33
- } from '../ui';
34
-
35
- interface AssetManagerProps {
36
- onSelect: (asset: {
37
- assetId: string;
38
- url: string;
39
- filename: string;
40
- mimeType: string;
41
- filesize: number;
42
- }) => void;
43
- onClose: () => void;
44
- allowedmime_types?: string[];
45
- maxFileSize?: number;
46
- bucket?: string;
47
- }
48
-
49
- export const AssetManagerModal: React.FC<AssetManagerProps> = ({
50
- onSelect,
51
- onClose,
52
- allowedmime_types,
53
- maxFileSize,
54
- bucket = 'default',
55
- }) => {
56
- const { data, loading: isLoading } = useStore($assets);
57
- const selectedBucket = useStore($mediaSelectedBucket);
58
- const currentFolder = useStore($mediaCurrentFolder);
59
- const viewMode = useStore($mediaViewMode);
60
- const config = useStore($config);
61
-
62
- const [isUploading, setIsUploading] = useState<boolean>(false);
63
- const [isDragActive, setIsDragActive] = useState<boolean>(false);
64
-
65
- const fileInputRef = useRef<HTMLInputElement>(null);
66
-
67
- const buckets = useMemo(() => {
68
- return Object.keys(config?.storages || {});
69
- }, [config?.storages]);
70
-
71
- const assets = data?.docs || [];
72
- const folders = data?.folders || [];
73
-
74
- const handleUpload = async (file: File) => {
75
- // ... same logic but use selectedBucket and currentFolder
76
- if (allowedmime_types && !allowedmime_types.includes(file.type)) {
77
- alert(`Invalid file type. Allowed: ${allowedmime_types.join(', ')}`);
78
- return;
79
- }
80
- if (maxFileSize && file.size > maxFileSize) {
81
- alert(`File too large. Max size: ${maxFileSize / 1024 / 1024}MB`);
82
- return;
83
- }
84
-
85
- setIsUploading(true);
86
-
87
- const formData = new FormData();
88
- formData.append('file', file);
89
-
90
- try {
91
- const bucketToUse = selectedBucket === 'all' ? 'default' : selectedBucket;
92
- const resp = await api
93
- .post(
94
- `api/__system/assets/upload?bucket=${bucketToUse}&folder=${currentFolder}`,
95
- {
96
- body: formData,
97
- onDownloadProgress: (_progress: unknown) => {
98
- // Ky uses onDownloadProgress, but xhr.upload.onprogress is for upload.
99
- },
100
- },
101
- )
102
- .json<AssetDoc>();
103
-
104
- setIsUploading(false);
105
- $assets.revalidate(); // Refresh the list
106
- // Auto select the new file
107
- onSelect({
108
- assetId: resp.id,
109
- url: resp.url || `${getCurrentBaseURL()}/api/assets/${resp.id}/view`,
110
- filename: resp.filename,
111
- mimeType: resp.mimeType || resp.mime_type || '',
112
- filesize: resp.filesize,
113
- });
114
- } catch (err: unknown) {
115
- alert(
116
- `Upload failed: ${err instanceof Error ? err.message : 'Unknown error'}`,
117
- );
118
- setIsUploading(false);
119
- }
120
- };
121
-
122
- const handleDrop = (e: React.DragEvent) => {
123
- e.preventDefault();
124
- setIsDragActive(false);
125
- if (
126
- e.dataTransfer.files &&
127
- e.dataTransfer.files.length > 0 &&
128
- e.dataTransfer.files[0]
129
- ) {
130
- handleUpload(e.dataTransfer.files[0]);
131
- }
132
- };
133
-
134
- const getFileIcon = (mime: string) => {
135
- if (!mime) return <File size={40} className="text-gray-400" />;
136
- if (mime.startsWith('image/'))
137
- return <ImageIcon size={40} className="text-blue-400" />;
138
- if (mime.startsWith('video/'))
139
- return <FileText size={40} className="text-purple-400" />;
140
- if (mime.includes('pdf'))
141
- return <FileText size={40} className="text-red-400" />;
142
- return <File size={40} className="text-gray-400" />;
143
- };
144
-
145
- return (
146
- <div className="asset-manager-overlay">
147
- <div className="asset-manager-container">
148
- {/* Header */}
149
- <div className="asset-manager-header">
150
- <div>
151
- <h2>Media Library</h2>
152
- <div className="asset-manager-breadcrumbs">
153
- <button type="button" onClick={() => setMediaFolder('')}>
154
- Home
155
- </button>
156
- {currentFolder
157
- .split('/')
158
- .filter(Boolean)
159
- .map((part: string, i: number, arr: string[]) => (
160
- <React.Fragment key={part || i}>
161
- <ChevronRight size={14} className="breadcrumb-separator" />
162
- <button
163
- type="button"
164
- onClick={() =>
165
- setMediaFolder(arr.slice(0, i + 1).join('/'))
166
- }
167
- >
168
- {part}
169
- </button>
170
- </React.Fragment>
171
- ))}
172
- </div>
173
- </div>
174
- <div className="header-actions">
175
- <Select
176
- value={selectedBucket}
177
- onValueChange={(val: string) => setMediaBucket(val)}
178
- >
179
- <SelectTrigger className="bucket-selector">
180
- <SelectValue placeholder="Bucket" />
181
- </SelectTrigger>
182
- <SelectContent>
183
- <SelectItem value="all">All Buckets</SelectItem>
184
- <SelectSeparator />
185
- <SelectLabel>Storage</SelectLabel>
186
- {buckets.map((b) => (
187
- <SelectItem key={b} value={b}>
188
- {b.toUpperCase()}
189
- </SelectItem>
190
- ))}
191
- </SelectContent>
192
- </Select>
193
- <button type="button" onClick={onClose} className="close-button">
194
- <X size={20} />
195
- </button>
196
- </div>
197
- </div>
198
-
199
- {/* Upload Zone */}
200
- <button
201
- type="button"
202
- onDragOver={(e) => {
203
- e.preventDefault();
204
- setIsDragActive(true);
205
- }}
206
- onDragLeave={() => setIsDragActive(false)}
207
- onDrop={handleDrop}
208
- onClick={() => !isUploading && fileInputRef.current?.click()}
209
- aria-label="Upload file"
210
- className={`asset-manager-upload-zone ${isDragActive ? 'is-drag-active' : ''} ${isUploading ? 'uploading' : ''}`}
211
- >
212
- <input
213
- type="file"
214
- ref={fileInputRef}
215
- style={{ display: 'none' }}
216
- accept={allowedmime_types?.join(',')}
217
- onChange={(e) => {
218
- if (
219
- e.target.files &&
220
- e.target.files.length > 0 &&
221
- e.target.files[0]
222
- ) {
223
- handleUpload(e.target.files[0]);
224
- }
225
- }}
226
- />
227
- {isUploading ? (
228
- <div className="uploading-status">
229
- <Loader2 className="opaca-spin" size={24} />
230
- <span className="status-text">Uploading...</span>
231
- </div>
232
- ) : (
233
- <>
234
- <Upload size={24} className="upload-icon" />
235
- <p className="upload-prompt">
236
- Drag & drop a file here, or <span>click to browse</span>.
237
- </p>
238
- </>
239
- )}
240
- </button>
241
-
242
- {/* Grid */}
243
- <div className="asset-manager-grid-container">
244
- {isLoading ? (
245
- <div className="loading-assets">
246
- <Loader2 className="opaca-spin" size={32} />
247
- <p>Loading assets...</p>
248
- </div>
249
- ) : assets.length === 0 && folders.length === 0 ? (
250
- <div className="no-assets">
251
- <ImageIcon size={48} className="empty-icon" />
252
- <p>No assets found here.</p>
253
- </div>
254
- ) : (
255
- <div className={`asset-manager-grid mode-${viewMode}`}>
256
- {/* Folders */}
257
- {folders.map((folder: any) => (
258
- <button
259
- type="button"
260
- key={`folder-${folder.name}`}
261
- className="asset-manager-card folder-card"
262
- onClick={() =>
263
- setMediaFolder(
264
- currentFolder
265
- ? `${currentFolder}/${folder.name}`
266
- : folder.name,
267
- )
268
- }
269
- >
270
- <div className="asset-thumb">
271
- <FolderPlus size={40} className="folder-icon" />
272
- </div>
273
- <div className="asset-info">
274
- <span className="filename">{folder.name}</span>
275
- <span className="file-meta">Folder</span>
276
- </div>
277
- </button>
278
- ))}
279
-
280
- {/* Assets */}
281
- {assets.map((asset: AssetDoc) => (
282
- <button
283
- type="button"
284
- key={asset.id}
285
- className="asset-manager-card asset-card"
286
- onClick={() => {
287
- const baseUrl = getCurrentBaseURL();
288
- onSelect({
289
- assetId: asset.id,
290
- url: `${baseUrl}/api/assets/${asset.id}/view`,
291
- filename: asset.filename,
292
- mimeType: asset.mimeType || asset.mime_type || '',
293
- filesize: asset.filesize,
294
- });
295
- }}
296
- >
297
- <div className="asset-thumb">
298
- {(() => {
299
- const mime = asset.mimeType || asset.mime_type;
300
- return mime?.startsWith('image/') ? (
301
- <img
302
- src={`${getCurrentBaseURL()}/api/assets/${asset.id}/view`}
303
- alt={asset.filename}
304
- />
305
- ) : (
306
- getFileIcon(mime || '')
307
- );
308
- })()}
309
- </div>
310
-
311
- <div className="asset-info">
312
- <span className="filename" title={asset.filename}>
313
- {asset.filename}
314
- </span>
315
- <span className="file-meta">
316
- {(asset.mimeType || asset.mime_type || '')
317
- .split('/')[1]
318
- ?.toUpperCase() || 'FILE'}{' '}
319
- • {((asset.filesize || 0) / 1024).toFixed(1)} KB
320
- </span>
321
- </div>
322
-
323
- <div className="selection-overlay">
324
- <div className="select-badge">Select</div>
325
- </div>
326
- </button>
327
- ))}
328
- </div>
329
- )}
330
- </div>
331
- </div>
332
- </div>
333
- );
334
- };
@@ -1,72 +0,0 @@
1
- import { CheckCircle, Info, X, XCircle } from "lucide-react";
2
- import { useEffect, useState } from "react";
3
- import type { ToastItem, ToastType } from "../../stores/ui";
4
-
5
- interface ToastProps extends ToastItem {
6
- onClear: (id: string) => void;
7
- }
8
-
9
- export function Toast({ id, message, type, onClear }: ToastProps) {
10
- const [isExiting, setIsExiting] = useState(false);
11
-
12
- useEffect(() => {
13
- const timer = setTimeout(() => {
14
- setIsExiting(true);
15
- setTimeout(() => onClear(id), 200);
16
- }, 4000);
17
- return () => clearTimeout(timer);
18
- }, [id, onClear]);
19
-
20
- const Icon = type === "success" ? CheckCircle : type === "error" ? XCircle : Info;
21
- const iconColor =
22
- type === "success"
23
- ? "var(--opaca-success)"
24
- : type === "error"
25
- ? "var(--opaca-error)"
26
- : "var(--opaca-accent)";
27
-
28
- return (
29
- <div className={`opaca-toast opaca-toast-${type} ${isExiting ? "exit" : ""}`}>
30
- <Icon size={18} style={{ color: iconColor }} />
31
- <span className="opaca-toast-message">{message}</span>
32
- <button
33
- type="button"
34
- onClick={() => {
35
- setIsExiting(true);
36
- setTimeout(() => onClear(id), 200);
37
- }}
38
- style={{
39
- background: "none",
40
- border: "none",
41
- padding: "4px",
42
- marginLeft: "auto",
43
- cursor: "pointer",
44
- color: "var(--opaca-text-dim)",
45
- display: "flex",
46
- alignItems: "center",
47
- justifyContent: "center",
48
- }}
49
- >
50
- <X size={14} />
51
- </button>
52
- </div>
53
- );
54
- }
55
-
56
- export function ToastContainer({
57
- toasts,
58
- onClear,
59
- }: {
60
- toasts: ToastItem[];
61
- onClear: (id: string) => void;
62
- }) {
63
- if (toasts.length === 0) return null;
64
-
65
- return (
66
- <div className="opaca-toast-container">
67
- {toasts.map((toast) => (
68
- <Toast key={toast.id} {...toast} onClear={onClear} />
69
- ))}
70
- </div>
71
- );
72
- }
@@ -1,51 +0,0 @@
1
- import * as React from "react";
2
- import { cn } from "./utils";
3
- import "../../styles/accordion.scss";
4
-
5
- interface AccordionProps {
6
- title: string;
7
- children: React.ReactNode;
8
- defaultOpen?: boolean;
9
- className?: string;
10
- isCollapsed?: boolean;
11
- }
12
-
13
- export const Accordion = ({
14
- title,
15
- children,
16
- defaultOpen = true,
17
- className,
18
- isCollapsed = false,
19
- }: AccordionProps) => {
20
- const [isOpen, setIsOpen] = React.useState(defaultOpen);
21
-
22
- return (
23
- <div className={cn("opaca-sidebar-accordion", className)}>
24
- <button
25
- type="button"
26
- className="opaca-sidebar-accordion-trigger"
27
- onClick={() => setIsOpen(!isOpen)}
28
- aria-expanded={isOpen}
29
- style={{
30
- opacity: isCollapsed ? 0 : 1,
31
- pointerEvents: isCollapsed ? "none" : "auto",
32
- }}
33
- title={title}
34
- >
35
- <span className="opaca-sidebar-accordion-title">{title}</span>
36
- <svg
37
- className="opaca-sidebar-accordion-icon"
38
- viewBox="0 0 24 24"
39
- xmlns="http://www.w3.org/2000/svg"
40
- aria-hidden="true"
41
- >
42
- <title>Toggle accordion</title>
43
- <polyline points="6 9 12 15 18 9"></polyline>
44
- </svg>
45
- </button>
46
- <div className="opaca-sidebar-accordion-content" data-state={isOpen ? "open" : "closed"}>
47
- <div className="opaca-sidebar-accordion-content-inner">{children}</div>
48
- </div>
49
- </div>
50
- );
51
- };
@@ -1,98 +0,0 @@
1
- import { Children, cloneElement, isValidElement, type ReactNode, useState } from "react";
2
- import { Button } from "./button";
3
- import {
4
- Dialog,
5
- DialogContent,
6
- DialogDescription,
7
- DialogFooter,
8
- DialogHeader,
9
- DialogTitle,
10
- } from "./dialog";
11
-
12
- export interface AlertDialogProps {
13
- children?: ReactNode;
14
- trigger?: ReactNode;
15
- title: string;
16
- description: string;
17
- onConfirm: () => void;
18
- confirmText?: string;
19
- cancelText?: string;
20
- variant?: "default" | "destructive";
21
- }
22
-
23
- export function AlertDialog({
24
- children,
25
- trigger,
26
- title,
27
- description,
28
- onConfirm,
29
- confirmText = "Continue",
30
- cancelText = "Cancel",
31
- variant = "default",
32
- }: AlertDialogProps) {
33
- const [open, setOpen] = useState(false);
34
-
35
- const handleConfirm = () => {
36
- onConfirm();
37
- setOpen(false);
38
- };
39
-
40
- const triggerElement = trigger || children;
41
-
42
- const renderTrigger = () => {
43
- if (Children.count(triggerElement) === 1 && isValidElement(triggerElement)) {
44
- const element = triggerElement as React.ReactElement<{
45
- onClick?: (e: React.MouseEvent) => void;
46
- }>;
47
- return cloneElement(element, {
48
- onClick: (e: React.MouseEvent) => {
49
- const originalOnClick = element.props.onClick;
50
- if (originalOnClick) originalOnClick(e);
51
- setOpen(true);
52
- },
53
- });
54
- }
55
-
56
- return (
57
- <span
58
- role="button"
59
- tabIndex={0}
60
- onClick={() => setOpen(true)}
61
- onKeyDown={(e) => {
62
- if (e.key === "Enter" || e.key === " ") {
63
- e.preventDefault();
64
- setOpen(true);
65
- }
66
- }}
67
- style={{
68
- display: "inline-block",
69
- cursor: "pointer",
70
- }}
71
- >
72
- {triggerElement}
73
- </span>
74
- );
75
- };
76
-
77
- return (
78
- <>
79
- {renderTrigger()}
80
- <Dialog open={open} onOpenChange={setOpen}>
81
- <DialogContent className="opaca-dialog-max-w">
82
- <DialogHeader>
83
- <DialogTitle>{title}</DialogTitle>
84
- <DialogDescription>{description}</DialogDescription>
85
- </DialogHeader>
86
- <DialogFooter>
87
- <Button variant="outline" onClick={() => setOpen(false)}>
88
- {cancelText}
89
- </Button>
90
- <Button variant={variant} onClick={handleConfirm}>
91
- {confirmText}
92
- </Button>
93
- </DialogFooter>
94
- </DialogContent>
95
- </Dialog>
96
- </>
97
- );
98
- }
@@ -1,32 +0,0 @@
1
- import * as React from "react";
2
- import { cn } from "./utils";
3
-
4
- export interface BlocksProps extends React.HTMLAttributes<HTMLDivElement> {
5
- label?: string;
6
- }
7
-
8
- export const Blocks = React.forwardRef<HTMLDivElement, BlocksProps>(
9
- ({ className, label, children, ...props }, ref) => {
10
- return (
11
- <div ref={ref} className={cn("opaca-ui-blocks", className)} {...props}>
12
- {label && (
13
- <div
14
- style={{
15
- marginBottom: "1.25rem",
16
- fontSize: "0.75rem",
17
- fontWeight: 500,
18
- color: "var(--opaca-text-muted)",
19
- textTransform: "uppercase",
20
- letterSpacing: "0.03em",
21
- }}
22
- >
23
- {label}
24
- </div>
25
- )}
26
- <div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>{children}</div>
27
- </div>
28
- );
29
- },
30
- );
31
-
32
- Blocks.displayName = "Blocks";
@@ -1,59 +0,0 @@
1
- import { ChevronRight, Home } from 'lucide-react';
2
- import React from 'react';
3
- import { Link } from '../link';
4
-
5
- export interface BreadcrumbItem {
6
- label: string;
7
- href?: string;
8
- }
9
-
10
- export function Breadcrumbs({ items }: { items: BreadcrumbItem[] }) {
11
- return (
12
- <nav
13
- aria-label="Breadcrumb"
14
- style={{
15
- display: 'flex',
16
- alignItems: 'center',
17
- gap: '0.5rem',
18
- marginBottom: '1.5rem',
19
- fontSize: '0.8125rem',
20
- color: 'var(--opaca-text-dim)',
21
- }}
22
- >
23
- <Link
24
- href="/admin"
25
- style={{
26
- display: 'flex',
27
- alignItems: 'center',
28
- color: 'inherit',
29
- textDecoration: 'none',
30
- transition: 'color var(--opaca-transition)',
31
- }}
32
- >
33
- <Home size={14} />
34
- </Link>
35
-
36
- {items.map((item, index) => (
37
- <React.Fragment key={item.label || index}>
38
- <ChevronRight size={12} style={{ opacity: 0.5 }} />
39
- {item.href ? (
40
- <Link
41
- href={item.href}
42
- style={{
43
- color: 'inherit',
44
- textDecoration: 'none',
45
- transition: 'color var(--opaca-transition)',
46
- }}
47
- >
48
- {item.label}
49
- </Link>
50
- ) : (
51
- <span style={{ color: 'var(--opaca-text)', fontWeight: '500' }}>
52
- {item.label}
53
- </span>
54
- )}
55
- </React.Fragment>
56
- ))}
57
- </nav>
58
- );
59
- }
@@ -1,26 +0,0 @@
1
- import * as React from "react";
2
- import { cn } from "./utils";
3
- import "../../styles/button.scss";
4
-
5
- export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
7
- size?: "default" | "sm" | "lg" | "icon";
8
- }
9
-
10
- export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
11
- ({ className, variant = "default", size = "default", ...props }, ref) => {
12
- return (
13
- <button
14
- ref={ref}
15
- className={cn(
16
- "opaca-ui-btn",
17
- `opaca-ui-btn-${variant}`,
18
- `opaca-ui-btn-size-${size}`,
19
- className,
20
- )}
21
- {...props}
22
- />
23
- );
24
- },
25
- );
26
- Button.displayName = "Button";