opacacms 0.1.12 → 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 (217) hide show
  1. package/dist/admin/index.js +0 -4
  2. package/dist/admin/webcomponent.d.ts +0 -1
  3. package/dist/admin/webcomponent.js +0 -4
  4. package/dist/admin.css +1 -0
  5. package/package.json +8 -2
  6. package/bun.lock +0 -34
  7. package/dist/admin/index.css +0 -47
  8. package/dist/admin/webcomponent.css +0 -47
  9. package/global.d.ts +0 -11
  10. package/src/admin/api-client.ts +0 -63
  11. package/src/admin/auth-client.ts +0 -40
  12. package/src/admin/custom-field.ts +0 -179
  13. package/src/admin/index.ts +0 -15
  14. package/src/admin/react.tsx +0 -72
  15. package/src/admin/router.ts +0 -9
  16. package/src/admin/stores/admin-queries.ts +0 -121
  17. package/src/admin/stores/auth.ts +0 -61
  18. package/src/admin/stores/column-visibility.ts +0 -67
  19. package/src/admin/stores/config.ts +0 -15
  20. package/src/admin/stores/media.ts +0 -95
  21. package/src/admin/stores/query.ts +0 -13
  22. package/src/admin/stores/ui.ts +0 -29
  23. package/src/admin/ui/admin-client.tsx +0 -283
  24. package/src/admin/ui/admin-layout.tsx +0 -276
  25. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  26. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  27. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  28. package/src/admin/ui/components/Table.tsx +0 -67
  29. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  30. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  31. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  32. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  33. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  34. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  35. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  36. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  37. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  38. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  39. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  40. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  41. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  42. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  43. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  44. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  45. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  46. package/src/admin/ui/components/fields/index.tsx +0 -371
  47. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  48. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  49. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  50. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  51. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  52. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  53. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  54. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  55. package/src/admin/ui/components/fields/utils.ts +0 -1
  56. package/src/admin/ui/components/link.tsx +0 -41
  57. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  58. package/src/admin/ui/components/toast.tsx +0 -72
  59. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  60. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  61. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  62. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  63. package/src/admin/ui/components/ui/button.tsx +0 -26
  64. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  65. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  66. package/src/admin/ui/components/ui/group.tsx +0 -20
  67. package/src/admin/ui/components/ui/index.ts +0 -17
  68. package/src/admin/ui/components/ui/input.tsx +0 -12
  69. package/src/admin/ui/components/ui/join.tsx +0 -53
  70. package/src/admin/ui/components/ui/label.tsx +0 -11
  71. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  72. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  73. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  74. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  75. package/src/admin/ui/components/ui/select.tsx +0 -187
  76. package/src/admin/ui/components/ui/separator.tsx +0 -21
  77. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  78. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  79. package/src/admin/ui/components/ui/utils.ts +0 -3
  80. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  81. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  82. package/src/admin/ui/styles/accordion.scss +0 -60
  83. package/src/admin/ui/styles/animations.scss +0 -41
  84. package/src/admin/ui/styles/asset-manager.scss +0 -547
  85. package/src/admin/ui/styles/badge.scss +0 -13
  86. package/src/admin/ui/styles/base.scss +0 -22
  87. package/src/admin/ui/styles/button.scss +0 -161
  88. package/src/admin/ui/styles/card.scss +0 -13
  89. package/src/admin/ui/styles/collapsible.scss +0 -75
  90. package/src/admin/ui/styles/data-detail.scss +0 -92
  91. package/src/admin/ui/styles/dialog.scss +0 -102
  92. package/src/admin/ui/styles/empty-state.scss +0 -22
  93. package/src/admin/ui/styles/group.scss +0 -19
  94. package/src/admin/ui/styles/index.scss +0 -33
  95. package/src/admin/ui/styles/input.scss +0 -80
  96. package/src/admin/ui/styles/label.scss +0 -12
  97. package/src/admin/ui/styles/layout.scss +0 -56
  98. package/src/admin/ui/styles/lexical.scss +0 -469
  99. package/src/admin/ui/styles/loading.scss +0 -102
  100. package/src/admin/ui/styles/media-registry.scss +0 -597
  101. package/src/admin/ui/styles/pagination.scss +0 -20
  102. package/src/admin/ui/styles/radio-group.scss +0 -66
  103. package/src/admin/ui/styles/row.scss +0 -17
  104. package/src/admin/ui/styles/scrollbar.scss +0 -36
  105. package/src/admin/ui/styles/select.scss +0 -121
  106. package/src/admin/ui/styles/separator.scss +0 -14
  107. package/src/admin/ui/styles/sheet.scss +0 -152
  108. package/src/admin/ui/styles/sidebar.scss +0 -148
  109. package/src/admin/ui/styles/switch.scss +0 -59
  110. package/src/admin/ui/styles/table.scss +0 -207
  111. package/src/admin/ui/styles/tabs.scss +0 -62
  112. package/src/admin/ui/styles/toast.scss +0 -45
  113. package/src/admin/ui/styles/variables.scss +0 -24
  114. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  115. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  116. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  117. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  118. package/src/admin/ui/views/init-view.tsx +0 -182
  119. package/src/admin/ui/views/login-view.tsx +0 -123
  120. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  121. package/src/admin/ui/views/settings-view.tsx +0 -729
  122. package/src/admin/webcomponent.tsx +0 -15
  123. package/src/auth/index.ts +0 -194
  124. package/src/auth/migrations.ts +0 -87
  125. package/src/auth/premissions.ts +0 -46
  126. package/src/cli/commands/generate-types.ts +0 -116
  127. package/src/cli/commands/init.ts +0 -95
  128. package/src/cli/commands/migrate-commands.ts +0 -160
  129. package/src/cli/commands/seed-command.ts +0 -11
  130. package/src/cli/d1-mock.ts +0 -101
  131. package/src/cli/index.test.ts +0 -84
  132. package/src/cli/index.ts +0 -183
  133. package/src/cli/r2-mock.ts +0 -217
  134. package/src/cli/seeding.ts +0 -409
  135. package/src/client.ts +0 -181
  136. package/src/config-utils.ts +0 -102
  137. package/src/config.ts +0 -49
  138. package/src/db/adapter.ts +0 -53
  139. package/src/db/better-sqlite.ts +0 -657
  140. package/src/db/bun-sqlite.ts +0 -666
  141. package/src/db/d1.ts +0 -721
  142. package/src/db/index.ts +0 -10
  143. package/src/db/kysely/data-mapper.ts +0 -142
  144. package/src/db/kysely/field-mapper.ts +0 -149
  145. package/src/db/kysely/migration-generator.ts +0 -223
  146. package/src/db/kysely/query-builder.ts +0 -92
  147. package/src/db/kysely/schema-builder.ts +0 -439
  148. package/src/db/kysely/sql-utils.ts +0 -13
  149. package/src/db/postgres.ts +0 -631
  150. package/src/db/sqlite.ts +0 -670
  151. package/src/db/system-schema.ts +0 -121
  152. package/src/index.ts +0 -13
  153. package/src/runtimes/README.md +0 -59
  154. package/src/runtimes/bun.ts +0 -49
  155. package/src/runtimes/cloudflare-workers.ts +0 -38
  156. package/src/runtimes/next.ts +0 -26
  157. package/src/runtimes/node.ts +0 -52
  158. package/src/schema/collection.ts +0 -184
  159. package/src/schema/fields/base.ts +0 -164
  160. package/src/schema/fields/index.ts +0 -427
  161. package/src/schema/global.ts +0 -145
  162. package/src/schema/index.ts +0 -4
  163. package/src/schema/infer.ts +0 -72
  164. package/src/server/admin-router.ts +0 -20
  165. package/src/server/admin.ts +0 -142
  166. package/src/server/assets.ts +0 -306
  167. package/src/server/collection-router.ts +0 -55
  168. package/src/server/handlers.ts +0 -722
  169. package/src/server/middlewares/admin.ts +0 -27
  170. package/src/server/middlewares/auth.ts +0 -89
  171. package/src/server/middlewares/context.ts +0 -17
  172. package/src/server/middlewares/cors.ts +0 -24
  173. package/src/server/middlewares/database-init.ts +0 -74
  174. package/src/server/middlewares/rate-limit.ts +0 -77
  175. package/src/server/router.ts +0 -47
  176. package/src/server/setup-middlewares.ts +0 -58
  177. package/src/server/system-router.ts +0 -35
  178. package/src/server.ts +0 -9
  179. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  180. package/src/storage/adapters/local.ts +0 -146
  181. package/src/storage/adapters/s3.ts +0 -186
  182. package/src/storage/errors.ts +0 -46
  183. package/src/storage/index.ts +0 -5
  184. package/src/storage/types.ts +0 -39
  185. package/src/types.ts +0 -577
  186. package/src/utils/lexical.ts +0 -37
  187. package/src/utils/logger.ts +0 -73
  188. package/src/validation.ts +0 -429
  189. package/src/validator.ts +0 -179
  190. package/test/admin-custom-field.test.ts +0 -162
  191. package/test/admin-react-field.test.tsx +0 -134
  192. package/test/api-features.test.ts +0 -78
  193. package/test/api.test.ts +0 -178
  194. package/test/auth.test.ts +0 -62
  195. package/test/cli-integration.test.ts +0 -148
  196. package/test/cli.test.ts +0 -25
  197. package/test/db/postgres.test.ts +0 -95
  198. package/test/db/sqlite-filter.test.ts +0 -53
  199. package/test/db/sqlite.test.ts +0 -82
  200. package/test/engine-features.test.ts +0 -79
  201. package/test/globals.test.ts +0 -74
  202. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  203. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  204. package/test/integration-tmp/my-test-app/index.ts +0 -8
  205. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  206. package/test/integration-tmp/my-test-app/package.json +0 -12
  207. package/test/populate.test.ts +0 -79
  208. package/test/runtimes.test.ts +0 -43
  209. package/test/schema-builder.test.ts +0 -107
  210. package/test/schema-features.test.ts +0 -63
  211. package/test/seeding.test.ts +0 -68
  212. package/test/storage/local.test.ts +0 -72
  213. package/test/storage/s3.test.ts +0 -60
  214. package/test/structural-data.test.ts +0 -100
  215. package/test/test-setup.ts +0 -11
  216. package/test/validation.test.ts +0 -162
  217. 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";