@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.
@@ -82,25 +82,95 @@ function scrollToSection(sectionId: string): void {
82
82
  }
83
83
 
84
84
  // --- Assets ---
85
- const uploadedFiles = ref<Array<{ name: string; size: string; type: string }>>([]);
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; size: number }>('/api/files/upload', { method: 'POST', body: formData })
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: `${(res.size / 1024).toFixed(0)} KB`,
100
- type: file.type.startsWith('image/') ? 'image' : 'file',
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
- .catch(() => { /* silent */ });
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
- <div class="cpub-ae-asset-icon">
281
- <i :class="file.type === 'image' ? 'fa-solid fa-image' : 'fa-solid fa-file'" />
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-size">{{ file.size }}</div>
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.1",
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/learning": "0.5.0",
60
- "@commonpub/ui": "0.8.5",
58
+ "@commonpub/schema": "0.9.6",
61
59
  "@commonpub/server": "2.28.0",
62
- "@commonpub/explainer": "0.7.10"
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",