opacacms 0.1.0 → 0.1.2

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 (247) hide show
  1. package/dist/admin/index.js +49 -0
  2. package/dist/{chunk-2zm8cy1w.js → admin/webcomponent.js} +116 -168
  3. package/dist/{chunk-6dhs73zq.js → chunk-2yz1nsxs.js} +1 -1
  4. package/dist/chunk-fa5mg0hr.js +96 -0
  5. package/dist/{chunk-kwp83w8b.js → chunk-m09hahe2.js} +7 -7
  6. package/dist/{chunk-hmhcense.js → chunk-ry15hke8.js} +253 -4
  7. package/dist/chunk-vtvqfhgy.js +2442 -0
  8. package/dist/{chunk-f3nvxn63.js → chunk-y8hc6nm4.js} +1 -1
  9. package/dist/{src/cli → cli}/index.js +10 -10
  10. package/dist/{src/client.js → client.js} +2 -2
  11. package/dist/{src/db → db}/bun-sqlite.js +10 -10
  12. package/dist/{src/db → db}/d1.js +8 -8
  13. package/dist/db/index.d.ts +2 -0
  14. package/dist/db/index.js +7 -0
  15. package/dist/db/migration.d.ts +39 -0
  16. package/dist/{src/db → db}/postgres.js +10 -10
  17. package/dist/{src/db → db}/sqlite.js +8 -8
  18. package/dist/index.d.ts +0 -2
  19. package/dist/index.js +13 -0
  20. package/dist/{src/runtimes → runtimes}/bun.js +5 -6
  21. package/dist/{src/runtimes → runtimes}/cloudflare-workers.js +5 -6
  22. package/dist/{src/runtimes → runtimes}/next.js +5 -6
  23. package/dist/{src/runtimes → runtimes}/node.js +5 -6
  24. package/dist/{src/server.js → server.js} +7 -8
  25. package/dist/storage/index.d.ts +0 -3
  26. package/dist/storage/index.js +35 -0
  27. package/dist/types.d.ts +5 -2
  28. package/package.json +161 -39
  29. package/bun.lock +0 -34
  30. package/dist/api.d.ts +0 -6
  31. package/dist/chunk-8gkhn1d4.js +0 -309
  32. package/dist/chunk-dy5t83hr.js +0 -261
  33. package/dist/src/admin/index.js +0 -176
  34. package/dist/src/admin/webcomponent.js +0 -19
  35. package/dist/src/api.js +0 -27
  36. package/dist/src/index.js +0 -20
  37. package/dist/src/storage/index.js +0 -355
  38. package/global.d.ts +0 -11
  39. package/src/admin/api-client.ts +0 -63
  40. package/src/admin/auth-client.ts +0 -40
  41. package/src/admin/custom-field.ts +0 -179
  42. package/src/admin/index.ts +0 -15
  43. package/src/admin/react.tsx +0 -72
  44. package/src/admin/router.ts +0 -9
  45. package/src/admin/stores/admin-queries.ts +0 -121
  46. package/src/admin/stores/auth.ts +0 -61
  47. package/src/admin/stores/column-visibility.ts +0 -67
  48. package/src/admin/stores/config.ts +0 -15
  49. package/src/admin/stores/media.ts +0 -95
  50. package/src/admin/stores/query.ts +0 -13
  51. package/src/admin/stores/ui.ts +0 -29
  52. package/src/admin/ui/admin-client.tsx +0 -283
  53. package/src/admin/ui/admin-layout.tsx +0 -276
  54. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  55. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  56. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  57. package/src/admin/ui/components/Table.tsx +0 -67
  58. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  59. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  60. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  61. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  62. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  63. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  64. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  65. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  66. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  67. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  68. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  69. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  70. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  71. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  72. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  73. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  74. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  75. package/src/admin/ui/components/fields/index.tsx +0 -371
  76. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  77. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  78. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  79. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  80. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  81. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  82. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  83. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  84. package/src/admin/ui/components/fields/utils.ts +0 -1
  85. package/src/admin/ui/components/link.tsx +0 -41
  86. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  87. package/src/admin/ui/components/toast.tsx +0 -72
  88. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  89. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  90. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  91. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  92. package/src/admin/ui/components/ui/button.tsx +0 -26
  93. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  94. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  95. package/src/admin/ui/components/ui/group.tsx +0 -20
  96. package/src/admin/ui/components/ui/index.ts +0 -17
  97. package/src/admin/ui/components/ui/input.tsx +0 -12
  98. package/src/admin/ui/components/ui/join.tsx +0 -53
  99. package/src/admin/ui/components/ui/label.tsx +0 -11
  100. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  101. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  102. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  103. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  104. package/src/admin/ui/components/ui/select.tsx +0 -187
  105. package/src/admin/ui/components/ui/separator.tsx +0 -21
  106. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  107. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  108. package/src/admin/ui/components/ui/utils.ts +0 -3
  109. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  110. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  111. package/src/admin/ui/styles/accordion.scss +0 -60
  112. package/src/admin/ui/styles/animations.scss +0 -41
  113. package/src/admin/ui/styles/asset-manager.scss +0 -547
  114. package/src/admin/ui/styles/badge.scss +0 -13
  115. package/src/admin/ui/styles/base.scss +0 -22
  116. package/src/admin/ui/styles/button.scss +0 -161
  117. package/src/admin/ui/styles/card.scss +0 -13
  118. package/src/admin/ui/styles/collapsible.scss +0 -75
  119. package/src/admin/ui/styles/data-detail.scss +0 -92
  120. package/src/admin/ui/styles/dialog.scss +0 -102
  121. package/src/admin/ui/styles/empty-state.scss +0 -22
  122. package/src/admin/ui/styles/group.scss +0 -19
  123. package/src/admin/ui/styles/index.scss +0 -33
  124. package/src/admin/ui/styles/input.scss +0 -80
  125. package/src/admin/ui/styles/label.scss +0 -12
  126. package/src/admin/ui/styles/layout.scss +0 -56
  127. package/src/admin/ui/styles/lexical.scss +0 -469
  128. package/src/admin/ui/styles/loading.scss +0 -102
  129. package/src/admin/ui/styles/media-registry.scss +0 -597
  130. package/src/admin/ui/styles/pagination.scss +0 -20
  131. package/src/admin/ui/styles/radio-group.scss +0 -66
  132. package/src/admin/ui/styles/row.scss +0 -17
  133. package/src/admin/ui/styles/scrollbar.scss +0 -36
  134. package/src/admin/ui/styles/select.scss +0 -121
  135. package/src/admin/ui/styles/separator.scss +0 -14
  136. package/src/admin/ui/styles/sheet.scss +0 -152
  137. package/src/admin/ui/styles/sidebar.scss +0 -148
  138. package/src/admin/ui/styles/switch.scss +0 -59
  139. package/src/admin/ui/styles/table.scss +0 -207
  140. package/src/admin/ui/styles/tabs.scss +0 -62
  141. package/src/admin/ui/styles/toast.scss +0 -45
  142. package/src/admin/ui/styles/variables.scss +0 -24
  143. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  144. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  145. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  146. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  147. package/src/admin/ui/views/init-view.tsx +0 -182
  148. package/src/admin/ui/views/login-view.tsx +0 -123
  149. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  150. package/src/admin/ui/views/settings-view.tsx +0 -729
  151. package/src/admin/webcomponent.tsx +0 -15
  152. package/src/api.ts +0 -9
  153. package/src/auth/index.ts +0 -194
  154. package/src/auth/migrations.ts +0 -87
  155. package/src/auth/premissions.ts +0 -46
  156. package/src/cli/commands/generate-types.ts +0 -116
  157. package/src/cli/commands/init.ts +0 -95
  158. package/src/cli/commands/migrate-commands.ts +0 -160
  159. package/src/cli/commands/seed-command.ts +0 -11
  160. package/src/cli/d1-mock.ts +0 -101
  161. package/src/cli/index.test.ts +0 -84
  162. package/src/cli/index.ts +0 -183
  163. package/src/cli/r2-mock.ts +0 -217
  164. package/src/cli/seeding.ts +0 -405
  165. package/src/client.ts +0 -181
  166. package/src/config-utils.ts +0 -102
  167. package/src/config.ts +0 -49
  168. package/src/db/adapter.ts +0 -53
  169. package/src/db/better-sqlite.ts +0 -630
  170. package/src/db/bun-sqlite.ts +0 -646
  171. package/src/db/d1.ts +0 -711
  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 -658
  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 -71
  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 -146
  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
  247. /package/dist/{src/admin/index.css → admin/webcomponent.css} +0 -0
@@ -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";