@commonpub/layer 0.8.1 → 0.8.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.
@@ -82,25 +82,77 @@ 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);
86
99
 
87
100
  function onAssetUpload(event: Event): void {
88
101
  const input = event.target as HTMLInputElement;
89
102
  if (!input.files?.length) return;
90
103
  const file = input.files[0];
91
104
  if (!file) return;
105
+ uploadError.value = '';
106
+
107
+ // Client-side size check
108
+ if (file.size > MAX_CONTENT_UPLOAD_BYTES) {
109
+ uploadError.value = `File too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Max ${MAX_CONTENT_UPLOAD_MB} MB.`;
110
+ input.value = '';
111
+ return;
112
+ }
113
+
114
+ uploading.value = true;
92
115
  const formData = new FormData();
93
116
  formData.append('file', file);
94
117
  formData.append('purpose', 'content');
95
- $fetch<{ url: string; originalName: string; size: number }>('/api/files/upload', { method: 'POST', body: formData })
118
+ $fetch<{ url: string; originalName: string; sizeBytes: number; mimeType: string }>('/api/files/upload', { method: 'POST', body: formData })
96
119
  .then((res) => {
97
120
  uploadedFiles.value.unshift({
98
121
  name: res.originalName || file.name,
99
- size: `${(res.size / 1024).toFixed(0)} KB`,
100
- type: file.type.startsWith('image/') ? 'image' : 'file',
122
+ size: res.sizeBytes < 1024 * 1024
123
+ ? `${(res.sizeBytes / 1024).toFixed(0)} KB`
124
+ : `${(res.sizeBytes / 1024 / 1024).toFixed(1)} MB`,
125
+ type: (res.mimeType || file.type).startsWith('image/') ? 'image' : 'file',
126
+ url: res.url,
127
+ mimeType: res.mimeType || file.type,
101
128
  });
129
+ uploadError.value = '';
130
+ })
131
+ .catch((err) => {
132
+ uploadError.value = err?.data?.statusMessage || 'Upload failed';
102
133
  })
103
- .catch(() => { /* silent */ });
134
+ .finally(() => {
135
+ uploading.value = false;
136
+ input.value = '';
137
+ });
138
+ }
139
+
140
+ function insertAsset(asset: UploadedAsset): void {
141
+ if (asset.type === 'image') {
142
+ // Insert image block after selected block (or at end)
143
+ const idx = props.blockEditor.selectedBlockId.value
144
+ ? props.blockEditor.getBlockIndex(props.blockEditor.selectedBlockId.value) + 1
145
+ : undefined;
146
+ props.blockEditor.addBlock('image', { url: asset.url, alt: asset.name }, idx);
147
+ } else {
148
+ // Copy URL to clipboard for non-image files
149
+ navigator.clipboard.writeText(asset.url).catch(() => {});
150
+ asset.name = `${asset.name} (copied!)`;
151
+ setTimeout(() => {
152
+ const found = uploadedFiles.value.find((f) => f.url === asset.url);
153
+ if (found) found.name = found.name.replace(' (copied!)', '');
154
+ }, 1500);
155
+ }
104
156
  }
105
157
 
106
158
  // --- Cover image ---
@@ -264,25 +316,34 @@ const canvasMaxWidth = computed(() => {
264
316
 
265
317
  <!-- Assets tab -->
266
318
  <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">
319
+ <label class="cpub-ae-assets-drop" :class="{ 'cpub-ae-assets-uploading': uploading }">
320
+ <i :class="uploading ? 'fa-solid fa-spinner fa-spin' : 'fa-solid fa-cloud-arrow-up'"></i>
321
+ <div class="cpub-ae-assets-drop-label">{{ uploading ? 'Uploading...' : 'Drop files here' }}</div>
322
+ <div class="cpub-ae-assets-drop-sub">JPG, PNG, GIF, SVG, PDF — max {{ MAX_CONTENT_UPLOAD_MB }} MB</div>
323
+ <input type="file" class="cpub-sr-only" :disabled="uploading" @change="onAssetUpload">
272
324
  </label>
325
+ <div v-if="uploadError" class="cpub-ae-assets-error">
326
+ <i class="fa-solid fa-triangle-exclamation"></i> {{ uploadError }}
327
+ </div>
273
328
  <div v-if="uploadedFiles.length > 0" class="cpub-ae-assets-list">
274
329
  <div class="cpub-ae-assets-heading">Recent Uploads</div>
275
330
  <div
276
331
  v-for="(file, idx) in uploadedFiles"
277
332
  :key="idx"
278
333
  class="cpub-ae-asset-item"
334
+ :title="file.type === 'image' ? 'Click to insert image' : 'Click to copy URL'"
335
+ @click="insertAsset(file)"
279
336
  >
280
- <div class="cpub-ae-asset-icon">
281
- <i :class="file.type === 'image' ? 'fa-solid fa-image' : 'fa-solid fa-file'" />
337
+ <img v-if="file.type === 'image'" :src="file.url" :alt="file.name" class="cpub-ae-asset-thumb" />
338
+ <div v-else class="cpub-ae-asset-icon">
339
+ <i class="fa-solid fa-file" />
282
340
  </div>
283
341
  <div class="cpub-ae-asset-info">
284
342
  <div class="cpub-ae-asset-name">{{ file.name }}</div>
285
- <div class="cpub-ae-asset-size">{{ file.size }}</div>
343
+ <div class="cpub-ae-asset-meta">
344
+ <span class="cpub-ae-asset-size">{{ file.size }}</span>
345
+ <span class="cpub-ae-asset-action">{{ file.type === 'image' ? 'insert' : 'copy url' }}</span>
346
+ </div>
286
347
  </div>
287
348
  </div>
288
349
  </div>
@@ -505,15 +566,26 @@ const canvasMaxWidth = computed(() => {
505
566
  transition: border-color 0.15s, background 0.15s; text-align: center;
506
567
  }
507
568
  .cpub-ae-assets-drop:hover { border-color: var(--accent); background: var(--accent-bg); }
569
+ .cpub-ae-assets-uploading { border-color: var(--accent); background: var(--accent-bg); pointer-events: none; }
508
570
  .cpub-ae-assets-drop i { font-size: 20px; color: var(--text-faint); }
509
571
  .cpub-ae-assets-drop-label { font-size: 11px; font-weight: 600; color: var(--text-dim); }
510
572
  .cpub-ae-assets-drop-sub { font-size: 10px; color: var(--text-faint); font-family: var(--font-mono); }
573
+ .cpub-ae-assets-error {
574
+ margin: 4px 8px 8px; padding: 6px 10px; font-size: 10px; font-family: var(--font-mono);
575
+ color: var(--red); background: var(--red-bg); border: var(--border-width-default) solid var(--red);
576
+ display: flex; align-items: center; gap: 6px;
577
+ }
511
578
  .cpub-ae-assets-list { padding: 8px 12px; }
512
579
  .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
580
  .cpub-ae-asset-item {
514
581
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
515
582
  background: var(--surface); border: var(--border-width-default) solid var(--border); cursor: pointer;
516
- box-shadow: var(--shadow-sm); margin-bottom: 5px;
583
+ box-shadow: var(--shadow-sm); margin-bottom: 5px; transition: background 0.1s, border-color 0.1s;
584
+ }
585
+ .cpub-ae-asset-item:hover { background: var(--surface2); border-color: var(--accent-border); }
586
+ .cpub-ae-asset-thumb {
587
+ width: 34px; height: 34px; object-fit: cover; flex-shrink: 0;
588
+ border: var(--border-width-default) solid var(--border2);
517
589
  }
518
590
  .cpub-ae-asset-icon {
519
591
  width: 34px; height: 34px; background: var(--surface2); display: flex;
@@ -522,7 +594,12 @@ const canvasMaxWidth = computed(() => {
522
594
  .cpub-ae-asset-icon i { font-size: 11px; color: var(--text-faint); }
523
595
  .cpub-ae-asset-info { flex: 1; min-width: 0; }
524
596
  .cpub-ae-asset-name { font-size: 10px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
597
+ .cpub-ae-asset-meta { display: flex; align-items: center; gap: 6px; }
525
598
  .cpub-ae-asset-size { font-family: var(--font-mono); font-size: 8px; color: var(--text-faint); }
599
+ .cpub-ae-asset-action {
600
+ font-family: var(--font-mono); font-size: 8px; color: var(--accent);
601
+ text-transform: uppercase; letter-spacing: 0.05em;
602
+ }
526
603
 
527
604
  /* Center */
528
605
  .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.2",
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/auth": "0.5.0",
54
- "@commonpub/docs": "0.6.2",
55
53
  "@commonpub/editor": "0.7.9",
56
- "@commonpub/schema": "0.9.6",
57
54
  "@commonpub/config": "0.9.1",
58
- "@commonpub/protocol": "0.9.8",
59
- "@commonpub/learning": "0.5.0",
55
+ "@commonpub/explainer": "0.7.10",
56
+ "@commonpub/docs": "0.6.2",
57
+ "@commonpub/auth": "0.5.0",
60
58
  "@commonpub/ui": "0.8.5",
59
+ "@commonpub/schema": "0.9.6",
60
+ "@commonpub/protocol": "0.9.8",
61
61
  "@commonpub/server": "2.28.0",
62
- "@commonpub/explainer": "0.7.10"
62
+ "@commonpub/learning": "0.5.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@testing-library/jest-dom": "^6.9.1",