@commonpub/layer 0.8.1 → 0.8.3
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.
- package/components/editors/ArticleEditor.vue +109 -14
- package/package.json +6 -6
|
@@ -82,25 +82,95 @@ function scrollToSection(sectionId: string): void {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// --- Assets ---
|
|
85
|
-
const
|
|
85
|
+
const MAX_CONTENT_UPLOAD_MB = 10;
|
|
86
|
+
const MAX_CONTENT_UPLOAD_BYTES = MAX_CONTENT_UPLOAD_MB * 1024 * 1024;
|
|
87
|
+
|
|
88
|
+
interface UploadedAsset {
|
|
89
|
+
name: string;
|
|
90
|
+
size: string;
|
|
91
|
+
type: 'image' | 'file';
|
|
92
|
+
url: string;
|
|
93
|
+
mimeType: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const uploadedFiles = ref<UploadedAsset[]>([]);
|
|
97
|
+
const uploadError = ref('');
|
|
98
|
+
const uploading = ref(false);
|
|
99
|
+
|
|
100
|
+
// Load existing uploads on mount
|
|
101
|
+
onMounted(async () => {
|
|
102
|
+
try {
|
|
103
|
+
const files = await $fetch<Array<{ originalName: string; sizeBytes: number; mimeType: string; url: string }>>('/api/files/mine?limit=30');
|
|
104
|
+
uploadedFiles.value = files.map((f) => ({
|
|
105
|
+
name: f.originalName,
|
|
106
|
+
size: f.sizeBytes < 1024 * 1024
|
|
107
|
+
? `${(f.sizeBytes / 1024).toFixed(0)} KB`
|
|
108
|
+
: `${(f.sizeBytes / 1024 / 1024).toFixed(1)} MB`,
|
|
109
|
+
type: f.mimeType.startsWith('image/') ? 'image' as const : 'file' as const,
|
|
110
|
+
url: f.url,
|
|
111
|
+
mimeType: f.mimeType,
|
|
112
|
+
}));
|
|
113
|
+
} catch {
|
|
114
|
+
// Not logged in or API unavailable — empty assets is fine
|
|
115
|
+
}
|
|
116
|
+
});
|
|
86
117
|
|
|
87
118
|
function onAssetUpload(event: Event): void {
|
|
88
119
|
const input = event.target as HTMLInputElement;
|
|
89
120
|
if (!input.files?.length) return;
|
|
90
121
|
const file = input.files[0];
|
|
91
122
|
if (!file) return;
|
|
123
|
+
uploadError.value = '';
|
|
124
|
+
|
|
125
|
+
// Client-side size check
|
|
126
|
+
if (file.size > MAX_CONTENT_UPLOAD_BYTES) {
|
|
127
|
+
uploadError.value = `File too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Max ${MAX_CONTENT_UPLOAD_MB} MB.`;
|
|
128
|
+
input.value = '';
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
uploading.value = true;
|
|
92
133
|
const formData = new FormData();
|
|
93
134
|
formData.append('file', file);
|
|
94
135
|
formData.append('purpose', 'content');
|
|
95
|
-
$fetch<{ url: string; originalName: string;
|
|
136
|
+
$fetch<{ url: string; originalName: string; sizeBytes: number; mimeType: string }>('/api/files/upload', { method: 'POST', body: formData })
|
|
96
137
|
.then((res) => {
|
|
97
138
|
uploadedFiles.value.unshift({
|
|
98
139
|
name: res.originalName || file.name,
|
|
99
|
-
size:
|
|
100
|
-
|
|
140
|
+
size: res.sizeBytes < 1024 * 1024
|
|
141
|
+
? `${(res.sizeBytes / 1024).toFixed(0)} KB`
|
|
142
|
+
: `${(res.sizeBytes / 1024 / 1024).toFixed(1)} MB`,
|
|
143
|
+
type: (res.mimeType || file.type).startsWith('image/') ? 'image' : 'file',
|
|
144
|
+
url: res.url,
|
|
145
|
+
mimeType: res.mimeType || file.type,
|
|
101
146
|
});
|
|
147
|
+
uploadError.value = '';
|
|
148
|
+
})
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
uploadError.value = err?.data?.statusMessage || 'Upload failed';
|
|
102
151
|
})
|
|
103
|
-
.
|
|
152
|
+
.finally(() => {
|
|
153
|
+
uploading.value = false;
|
|
154
|
+
input.value = '';
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function insertAsset(asset: UploadedAsset): void {
|
|
159
|
+
if (asset.type === 'image') {
|
|
160
|
+
// Insert image block after selected block (or at end)
|
|
161
|
+
const idx = props.blockEditor.selectedBlockId.value
|
|
162
|
+
? props.blockEditor.getBlockIndex(props.blockEditor.selectedBlockId.value) + 1
|
|
163
|
+
: undefined;
|
|
164
|
+
props.blockEditor.addBlock('image', { src: asset.url, alt: asset.name }, idx);
|
|
165
|
+
} else {
|
|
166
|
+
// Copy URL to clipboard for non-image files
|
|
167
|
+
navigator.clipboard.writeText(asset.url).catch(() => {});
|
|
168
|
+
asset.name = `${asset.name} (copied!)`;
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
const found = uploadedFiles.value.find((f) => f.url === asset.url);
|
|
171
|
+
if (found) found.name = found.name.replace(' (copied!)', '');
|
|
172
|
+
}, 1500);
|
|
173
|
+
}
|
|
104
174
|
}
|
|
105
175
|
|
|
106
176
|
// --- Cover image ---
|
|
@@ -264,25 +334,34 @@ const canvasMaxWidth = computed(() => {
|
|
|
264
334
|
|
|
265
335
|
<!-- Assets tab -->
|
|
266
336
|
<div v-else class="cpub-ae-left-body">
|
|
267
|
-
<label class="cpub-ae-assets-drop">
|
|
268
|
-
<i class="fa-solid fa-cloud-arrow-up"></i>
|
|
269
|
-
<div class="cpub-ae-assets-drop-label">Drop files here</div>
|
|
270
|
-
<div class="cpub-ae-assets-drop-sub">JPG, PNG, GIF, SVG, PDF</div>
|
|
271
|
-
<input type="file" class="cpub-sr-only" @change="onAssetUpload">
|
|
337
|
+
<label class="cpub-ae-assets-drop" :class="{ 'cpub-ae-assets-uploading': uploading }">
|
|
338
|
+
<i :class="uploading ? 'fa-solid fa-spinner fa-spin' : 'fa-solid fa-cloud-arrow-up'"></i>
|
|
339
|
+
<div class="cpub-ae-assets-drop-label">{{ uploading ? 'Uploading...' : 'Drop files here' }}</div>
|
|
340
|
+
<div class="cpub-ae-assets-drop-sub">JPG, PNG, GIF, SVG, PDF — max {{ MAX_CONTENT_UPLOAD_MB }} MB</div>
|
|
341
|
+
<input type="file" class="cpub-sr-only" :disabled="uploading" @change="onAssetUpload">
|
|
272
342
|
</label>
|
|
343
|
+
<div v-if="uploadError" class="cpub-ae-assets-error">
|
|
344
|
+
<i class="fa-solid fa-triangle-exclamation"></i> {{ uploadError }}
|
|
345
|
+
</div>
|
|
273
346
|
<div v-if="uploadedFiles.length > 0" class="cpub-ae-assets-list">
|
|
274
347
|
<div class="cpub-ae-assets-heading">Recent Uploads</div>
|
|
275
348
|
<div
|
|
276
349
|
v-for="(file, idx) in uploadedFiles"
|
|
277
350
|
:key="idx"
|
|
278
351
|
class="cpub-ae-asset-item"
|
|
352
|
+
:title="file.type === 'image' ? 'Click to insert image' : 'Click to copy URL'"
|
|
353
|
+
@click="insertAsset(file)"
|
|
279
354
|
>
|
|
280
|
-
<
|
|
281
|
-
|
|
355
|
+
<img v-if="file.type === 'image'" :src="file.url" :alt="file.name" class="cpub-ae-asset-thumb" />
|
|
356
|
+
<div v-else class="cpub-ae-asset-icon">
|
|
357
|
+
<i class="fa-solid fa-file" />
|
|
282
358
|
</div>
|
|
283
359
|
<div class="cpub-ae-asset-info">
|
|
284
360
|
<div class="cpub-ae-asset-name">{{ file.name }}</div>
|
|
285
|
-
<div class="cpub-ae-asset-
|
|
361
|
+
<div class="cpub-ae-asset-meta">
|
|
362
|
+
<span class="cpub-ae-asset-size">{{ file.size }}</span>
|
|
363
|
+
<span class="cpub-ae-asset-action">{{ file.type === 'image' ? 'insert' : 'copy url' }}</span>
|
|
364
|
+
</div>
|
|
286
365
|
</div>
|
|
287
366
|
</div>
|
|
288
367
|
</div>
|
|
@@ -505,15 +584,26 @@ const canvasMaxWidth = computed(() => {
|
|
|
505
584
|
transition: border-color 0.15s, background 0.15s; text-align: center;
|
|
506
585
|
}
|
|
507
586
|
.cpub-ae-assets-drop:hover { border-color: var(--accent); background: var(--accent-bg); }
|
|
587
|
+
.cpub-ae-assets-uploading { border-color: var(--accent); background: var(--accent-bg); pointer-events: none; }
|
|
508
588
|
.cpub-ae-assets-drop i { font-size: 20px; color: var(--text-faint); }
|
|
509
589
|
.cpub-ae-assets-drop-label { font-size: 11px; font-weight: 600; color: var(--text-dim); }
|
|
510
590
|
.cpub-ae-assets-drop-sub { font-size: 10px; color: var(--text-faint); font-family: var(--font-mono); }
|
|
591
|
+
.cpub-ae-assets-error {
|
|
592
|
+
margin: 4px 8px 8px; padding: 6px 10px; font-size: 10px; font-family: var(--font-mono);
|
|
593
|
+
color: var(--red); background: var(--red-bg); border: var(--border-width-default) solid var(--red);
|
|
594
|
+
display: flex; align-items: center; gap: 6px;
|
|
595
|
+
}
|
|
511
596
|
.cpub-ae-assets-list { padding: 8px 12px; }
|
|
512
597
|
.cpub-ae-assets-heading { font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-faint); padding: 4px 0 10px; }
|
|
513
598
|
.cpub-ae-asset-item {
|
|
514
599
|
display: flex; align-items: center; gap: 10px; padding: 8px 10px;
|
|
515
600
|
background: var(--surface); border: var(--border-width-default) solid var(--border); cursor: pointer;
|
|
516
|
-
box-shadow: var(--shadow-sm); margin-bottom: 5px;
|
|
601
|
+
box-shadow: var(--shadow-sm); margin-bottom: 5px; transition: background 0.1s, border-color 0.1s;
|
|
602
|
+
}
|
|
603
|
+
.cpub-ae-asset-item:hover { background: var(--surface2); border-color: var(--accent-border); }
|
|
604
|
+
.cpub-ae-asset-thumb {
|
|
605
|
+
width: 34px; height: 34px; object-fit: cover; flex-shrink: 0;
|
|
606
|
+
border: var(--border-width-default) solid var(--border2);
|
|
517
607
|
}
|
|
518
608
|
.cpub-ae-asset-icon {
|
|
519
609
|
width: 34px; height: 34px; background: var(--surface2); display: flex;
|
|
@@ -522,7 +612,12 @@ const canvasMaxWidth = computed(() => {
|
|
|
522
612
|
.cpub-ae-asset-icon i { font-size: 11px; color: var(--text-faint); }
|
|
523
613
|
.cpub-ae-asset-info { flex: 1; min-width: 0; }
|
|
524
614
|
.cpub-ae-asset-name { font-size: 10px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
|
|
615
|
+
.cpub-ae-asset-meta { display: flex; align-items: center; gap: 6px; }
|
|
525
616
|
.cpub-ae-asset-size { font-family: var(--font-mono); font-size: 8px; color: var(--text-faint); }
|
|
617
|
+
.cpub-ae-asset-action {
|
|
618
|
+
font-family: var(--font-mono); font-size: 8px; color: var(--accent);
|
|
619
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
620
|
+
}
|
|
526
621
|
|
|
527
622
|
/* Center */
|
|
528
623
|
.cpub-ae-center { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"vue": "^3.4.0",
|
|
51
51
|
"vue-router": "^4.3.0",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
+
"@commonpub/editor": "0.7.9",
|
|
53
54
|
"@commonpub/auth": "0.5.0",
|
|
54
55
|
"@commonpub/docs": "0.6.2",
|
|
55
|
-
"@commonpub/editor": "0.7.9",
|
|
56
|
-
"@commonpub/schema": "0.9.6",
|
|
57
56
|
"@commonpub/config": "0.9.1",
|
|
58
57
|
"@commonpub/protocol": "0.9.8",
|
|
59
|
-
"@commonpub/
|
|
60
|
-
"@commonpub/ui": "0.8.5",
|
|
58
|
+
"@commonpub/schema": "0.9.6",
|
|
61
59
|
"@commonpub/server": "2.28.0",
|
|
62
|
-
"@commonpub/
|
|
60
|
+
"@commonpub/learning": "0.5.0",
|
|
61
|
+
"@commonpub/explainer": "0.7.10",
|
|
62
|
+
"@commonpub/ui": "0.8.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|