@commonpub/layer 0.7.2 → 0.7.4

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 (64) hide show
  1. package/components/CommentSection.vue +4 -4
  2. package/components/ContentAttachments.vue +4 -4
  3. package/components/DiscussionItem.vue +1 -1
  4. package/components/FeedItem.vue +1 -1
  5. package/components/MessageThread.vue +1 -1
  6. package/components/NotificationItem.vue +1 -1
  7. package/components/editors/ArticleEditor.vue +11 -12
  8. package/components/editors/BlogEditor.vue +17 -18
  9. package/components/editors/ExplainerEditor.vue +13 -14
  10. package/components/editors/ProjectEditor.vue +17 -18
  11. package/components/hub/HubHero.vue +2 -2
  12. package/components/hub/HubLayout.vue +1 -1
  13. package/components/hub/HubProducts.vue +2 -2
  14. package/components/hub/HubSidebarCard.vue +2 -2
  15. package/components/views/ArticleView.vue +15 -15
  16. package/components/views/BlogView.vue +14 -14
  17. package/components/views/ExplainerView.vue +11 -11
  18. package/components/views/ProjectView.vue +36 -36
  19. package/composables/useMarkdownImport.ts +1 -1
  20. package/package.json +9 -9
  21. package/pages/admin/theme.vue +1 -1
  22. package/pages/authorize_interaction.vue +1 -1
  23. package/pages/docs/[siteSlug]/edit.vue +4 -4
  24. package/pages/federated-hubs/[id]/index.vue +6 -6
  25. package/pages/federated-hubs/[id]/posts/[postId].vue +5 -5
  26. package/pages/hubs/[slug]/index.vue +6 -6
  27. package/pages/hubs/[slug]/posts/[postId].vue +6 -6
  28. package/pages/hubs/index.vue +2 -2
  29. package/pages/mirror/[id].vue +2 -2
  30. package/pages/u/[username]/[type]/[slug]/edit.vue +2 -1
  31. package/server/api/docs/[siteSlug]/search.get.ts +2 -1
  32. package/theme/components.css +2 -2
  33. package/theme/layouts.css +2 -2
  34. package/theme/prose.css +4 -4
  35. package/components/editors/BlockCanvas.vue +0 -487
  36. package/components/editors/BlockInsertZone.vue +0 -84
  37. package/components/editors/BlockPicker.vue +0 -285
  38. package/components/editors/BlockWrapper.vue +0 -192
  39. package/components/editors/EditorBlocks.vue +0 -248
  40. package/components/editors/EditorSection.vue +0 -81
  41. package/components/editors/EditorShell.vue +0 -196
  42. package/components/editors/EditorTagInput.vue +0 -114
  43. package/components/editors/EditorVisibility.vue +0 -110
  44. package/components/editors/blocks/BuildStepBlock.vue +0 -102
  45. package/components/editors/blocks/CalloutBlock.vue +0 -122
  46. package/components/editors/blocks/CheckpointBlock.vue +0 -27
  47. package/components/editors/blocks/CodeBlock.vue +0 -177
  48. package/components/editors/blocks/DividerBlock.vue +0 -22
  49. package/components/editors/blocks/DownloadsBlock.vue +0 -41
  50. package/components/editors/blocks/EmbedBlock.vue +0 -20
  51. package/components/editors/blocks/GalleryBlock.vue +0 -236
  52. package/components/editors/blocks/HeadingBlock.vue +0 -96
  53. package/components/editors/blocks/ImageBlock.vue +0 -271
  54. package/components/editors/blocks/MarkdownBlock.vue +0 -258
  55. package/components/editors/blocks/MathBlock.vue +0 -37
  56. package/components/editors/blocks/PartsListBlock.vue +0 -358
  57. package/components/editors/blocks/QuizBlock.vue +0 -47
  58. package/components/editors/blocks/QuoteBlock.vue +0 -101
  59. package/components/editors/blocks/SectionHeaderBlock.vue +0 -130
  60. package/components/editors/blocks/SliderBlock.vue +0 -318
  61. package/components/editors/blocks/TextBlock.vue +0 -201
  62. package/components/editors/blocks/ToolListBlock.vue +0 -70
  63. package/components/editors/blocks/VideoBlock.vue +0 -22
  64. package/composables/useBlockEditor.ts +0 -187
@@ -1,236 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Gallery block — multi-image grid with upload and reorder.
4
- * Each image has src, alt, caption.
5
- */
6
- const props = defineProps<{
7
- content: Record<string, unknown>;
8
- }>();
9
-
10
- const emit = defineEmits<{
11
- update: [content: Record<string, unknown>];
12
- }>();
13
-
14
- interface GalleryImage {
15
- src: string;
16
- alt: string;
17
- caption: string;
18
- }
19
-
20
- const images = computed<GalleryImage[]>(() => {
21
- const raw = props.content.images;
22
- if (!Array.isArray(raw)) return [];
23
- return raw as GalleryImage[];
24
- });
25
-
26
- function updateImages(newImages: GalleryImage[]): void {
27
- emit('update', { images: newImages });
28
- }
29
-
30
- const uploading = ref(false);
31
-
32
- async function handleFileSelect(event: Event): Promise<void> {
33
- const input = event.target as HTMLInputElement;
34
- if (!input.files?.length) return;
35
-
36
- uploading.value = true;
37
- const newImages = [...images.value];
38
-
39
- for (const file of Array.from(input.files)) {
40
- try {
41
- const formData = new FormData();
42
- formData.append('file', file);
43
- formData.append('purpose', 'content');
44
- const result = await $fetch<{ url: string }>('/api/files/upload', {
45
- method: 'POST',
46
- body: formData,
47
- });
48
- newImages.push({
49
- src: result.url,
50
- alt: file.name.replace(/\.[^.]+$/, ''),
51
- caption: '',
52
- });
53
- } catch {
54
- // Skip failed uploads
55
- }
56
- }
57
-
58
- updateImages(newImages);
59
- uploading.value = false;
60
- input.value = '';
61
- }
62
-
63
- function removeImage(index: number): void {
64
- const newImages = images.value.filter((_, i) => i !== index);
65
- updateImages(newImages);
66
- }
67
-
68
- function updateImageField(index: number, field: keyof GalleryImage, value: string): void {
69
- const newImages = images.value.map((img, i) =>
70
- i === index ? { ...img, [field]: value } : img,
71
- );
72
- updateImages(newImages);
73
- }
74
- </script>
75
-
76
- <template>
77
- <div class="cpub-gallery-block">
78
- <!-- Image grid -->
79
- <div v-if="images.length > 0" class="cpub-gallery-grid">
80
- <div v-for="(img, idx) in images" :key="idx" class="cpub-gallery-item">
81
- <div class="cpub-gallery-img-wrap">
82
- <img :src="img.src" :alt="img.alt" class="cpub-gallery-img" />
83
- <button class="cpub-gallery-remove" title="Remove" @click="removeImage(idx)">
84
- <i class="fa-solid fa-xmark"></i>
85
- </button>
86
- </div>
87
- <input
88
- class="cpub-gallery-field"
89
- type="text"
90
- :value="img.alt"
91
- placeholder="Alt text..."
92
- @input="updateImageField(idx, 'alt', ($event.target as HTMLInputElement).value)"
93
- />
94
- <input
95
- class="cpub-gallery-field"
96
- type="text"
97
- :value="img.caption"
98
- placeholder="Caption..."
99
- @input="updateImageField(idx, 'caption', ($event.target as HTMLInputElement).value)"
100
- />
101
- </div>
102
- </div>
103
-
104
- <!-- Add images -->
105
- <label class="cpub-gallery-add" :class="{ 'cpub-gallery-uploading': uploading }">
106
- <input type="file" accept="image/*" multiple class="cpub-sr-only" :disabled="uploading" @change="handleFileSelect" />
107
- <template v-if="uploading">
108
- <i class="fa-solid fa-circle-notch fa-spin"></i>
109
- <span>Uploading...</span>
110
- </template>
111
- <template v-else>
112
- <i class="fa-solid fa-images"></i>
113
- <span>{{ images.length > 0 ? 'Add more images' : 'Upload images for gallery' }}</span>
114
- </template>
115
- </label>
116
- </div>
117
- </template>
118
-
119
- <style scoped>
120
- .cpub-gallery-block {
121
- display: flex;
122
- flex-direction: column;
123
- gap: 8px;
124
- }
125
-
126
- .cpub-gallery-grid {
127
- display: grid;
128
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
129
- gap: 8px;
130
- }
131
-
132
- .cpub-gallery-item {
133
- display: flex;
134
- flex-direction: column;
135
- gap: 4px;
136
- }
137
-
138
- .cpub-gallery-img-wrap {
139
- position: relative;
140
- aspect-ratio: 4/3;
141
- overflow: hidden;
142
- border: var(--border-width-default) solid var(--border);
143
- background: var(--surface2);
144
- }
145
-
146
- .cpub-gallery-img {
147
- width: 100%;
148
- height: 100%;
149
- object-fit: cover;
150
- }
151
-
152
- .cpub-gallery-remove {
153
- position: absolute;
154
- top: 4px;
155
- right: 4px;
156
- width: 22px;
157
- height: 22px;
158
- background: var(--text);
159
- border: none;
160
- color: var(--surface);
161
- font-size: 10px;
162
- cursor: pointer;
163
- display: flex;
164
- align-items: center;
165
- justify-content: center;
166
- opacity: 0;
167
- transition: opacity 0.12s;
168
- }
169
-
170
- .cpub-gallery-img-wrap:hover .cpub-gallery-remove {
171
- opacity: 1;
172
- }
173
-
174
- .cpub-gallery-remove:hover {
175
- background: var(--red);
176
- }
177
-
178
- .cpub-gallery-field {
179
- width: 100%;
180
- padding: 4px 6px;
181
- font-size: 10px;
182
- background: var(--surface);
183
- border: var(--border-width-default) solid var(--border2);
184
- color: var(--text-dim);
185
- outline: none;
186
- font-style: italic;
187
- }
188
-
189
- .cpub-gallery-field:focus {
190
- border-color: var(--accent);
191
- font-style: normal;
192
- }
193
-
194
- .cpub-gallery-field::placeholder {
195
- color: var(--text-faint);
196
- }
197
-
198
- .cpub-gallery-add {
199
- display: flex;
200
- align-items: center;
201
- justify-content: center;
202
- gap: 8px;
203
- padding: 24px;
204
- border: 2px dashed var(--border2);
205
- cursor: pointer;
206
- font-size: 12px;
207
- color: var(--text-dim);
208
- transition: all 0.12s;
209
- }
210
-
211
- .cpub-gallery-add:hover {
212
- border-color: var(--accent);
213
- background: var(--accent-bg);
214
- color: var(--accent);
215
- }
216
-
217
- .cpub-gallery-add i {
218
- font-size: 16px;
219
- }
220
-
221
- .cpub-gallery-uploading {
222
- pointer-events: none;
223
- opacity: 0.7;
224
- }
225
-
226
- .cpub-sr-only {
227
- position: absolute;
228
- width: 1px;
229
- height: 1px;
230
- padding: 0;
231
- margin: -1px;
232
- overflow: hidden;
233
- clip: rect(0, 0, 0, 0);
234
- border: 0;
235
- }
236
- </style>
@@ -1,96 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Heading block — editable heading with level selector.
4
- */
5
- const props = defineProps<{
6
- content: Record<string, unknown>;
7
- }>();
8
-
9
- const emit = defineEmits<{
10
- update: [content: Record<string, unknown>];
11
- }>();
12
-
13
- const level = computed(() => (props.content.level as number) ?? 2);
14
- const text = computed(() => (props.content.text as string) ?? '');
15
- const headingEl = ref<HTMLElement | null>(null);
16
-
17
- function onTextInput(event: Event): void {
18
- const el = event.target as HTMLElement;
19
- emit('update', { text: el.textContent ?? '', level: level.value });
20
- }
21
-
22
- function cycleLevel(): void {
23
- const next = level.value >= 4 ? 2 : level.value + 1;
24
- emit('update', { text: text.value, level: next });
25
- }
26
-
27
- // Set initial text without v-text (which fights with contenteditable)
28
- onMounted(() => {
29
- if (headingEl.value && text.value) {
30
- headingEl.value.textContent = text.value;
31
- }
32
- });
33
- </script>
34
-
35
- <template>
36
- <div class="cpub-heading-block">
37
- <button class="cpub-heading-level" title="Change heading level" aria-label="Change heading level" @click="cycleLevel">
38
- H{{ level }}
39
- </button>
40
- <div
41
- ref="headingEl"
42
- class="cpub-heading-text"
43
- :class="`cpub-heading-text--h${level}`"
44
- contenteditable="true"
45
- :data-placeholder="`Heading ${level}`"
46
- @input="onTextInput"
47
- />
48
- </div>
49
- </template>
50
-
51
- <style scoped>
52
- .cpub-heading-block {
53
- display: flex;
54
- align-items: flex-start;
55
- gap: 10px;
56
- }
57
-
58
- .cpub-heading-level {
59
- font-family: var(--font-mono);
60
- font-size: 10px;
61
- font-weight: 700;
62
- color: var(--text-faint);
63
- background: var(--surface2);
64
- border: var(--border-width-default) solid var(--border2);
65
- padding: 2px 6px;
66
- cursor: pointer;
67
- flex-shrink: 0;
68
- margin-top: 4px;
69
- transition: all 0.1s;
70
- }
71
-
72
- .cpub-heading-level:hover {
73
- border-color: var(--accent);
74
- color: var(--accent);
75
- background: var(--accent-bg);
76
- }
77
-
78
- .cpub-heading-text {
79
- flex: 1;
80
- font-weight: 700;
81
- outline: none;
82
- min-height: 1.2em;
83
- line-height: 1.3;
84
- }
85
-
86
- .cpub-heading-text:empty::before {
87
- content: attr(data-placeholder);
88
- color: var(--text-faint);
89
- pointer-events: none;
90
- }
91
-
92
- .cpub-heading-text--h1 { font-size: 28px; }
93
- .cpub-heading-text--h2 { font-size: 22px; }
94
- .cpub-heading-text--h3 { font-size: 18px; }
95
- .cpub-heading-text--h4 { font-size: 15px; }
96
- </style>
@@ -1,271 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Image block — upload placeholder or image preview with alt text and caption.
4
- */
5
- const props = defineProps<{
6
- content: Record<string, unknown>;
7
- }>();
8
-
9
- const emit = defineEmits<{
10
- update: [content: Record<string, unknown>];
11
- }>();
12
-
13
- const src = computed(() => (props.content.src as string) ?? '');
14
- const alt = computed(() => (props.content.alt as string) ?? '');
15
- const caption = computed(() => (props.content.caption as string) ?? '');
16
- const hasImage = computed(() => !!src.value);
17
-
18
- function updateField(field: string, value: string): void {
19
- emit('update', { ...props.content, [field]: value });
20
- }
21
-
22
- const uploading = ref(false);
23
- const uploadError = ref('');
24
-
25
- async function handleFileSelect(event: Event): Promise<void> {
26
- const input = event.target as HTMLInputElement;
27
- const file = input.files?.[0];
28
- if (!file) return;
29
-
30
- uploading.value = true;
31
- uploadError.value = '';
32
-
33
- try {
34
- const formData = new FormData();
35
- formData.append('file', file);
36
- formData.append('purpose', 'content');
37
-
38
- const result = await $fetch<{ url: string; width: number | null; height: number | null }>('/api/files/upload', {
39
- method: 'POST',
40
- body: formData,
41
- });
42
-
43
- emit('update', {
44
- src: result.url,
45
- alt: alt.value || file.name.replace(/\.[^.]+$/, ''),
46
- caption: caption.value,
47
- });
48
- } catch (err: unknown) {
49
- uploadError.value = (err as { data?: { statusMessage?: string } })?.data?.statusMessage ?? 'Upload failed';
50
- } finally {
51
- uploading.value = false;
52
- }
53
- }
54
- </script>
55
-
56
- <template>
57
- <div class="cpub-image-block">
58
- <!-- No image — upload placeholder -->
59
- <template v-if="!hasImage">
60
- <label class="cpub-image-placeholder" :class="{ 'cpub-image-uploading': uploading }">
61
- <input type="file" accept="image/*" class="cpub-sr-only" :disabled="uploading" @change="handleFileSelect" />
62
- <template v-if="uploading">
63
- <i class="fa-solid fa-circle-notch fa-spin cpub-image-placeholder-icon"></i>
64
- <span class="cpub-image-placeholder-text">Uploading...</span>
65
- </template>
66
- <template v-else>
67
- <i class="fa-regular fa-image cpub-image-placeholder-icon"></i>
68
- <span class="cpub-image-placeholder-text">Click to upload image or drag and drop</span>
69
- <span class="cpub-image-placeholder-hint">JPG, PNG, WebP, GIF · 10 MB max</span>
70
- </template>
71
- </label>
72
- <div v-if="uploadError" class="cpub-image-error">{{ uploadError }}</div>
73
- <div class="cpub-image-url-row">
74
- <input
75
- class="cpub-image-url-input"
76
- type="url"
77
- placeholder="Or paste image URL..."
78
- :value="src"
79
- @input="updateField('src', ($event.target as HTMLInputElement).value)"
80
- />
81
- </div>
82
- </template>
83
-
84
- <!-- Has image — show preview -->
85
- <template v-else>
86
- <div class="cpub-image-preview">
87
- <img :src="src" :alt="alt" class="cpub-image-preview-img" />
88
- <button class="cpub-image-remove" title="Remove image" @click="updateField('src', '')">
89
- <i class="fa-solid fa-xmark"></i>
90
- </button>
91
- </div>
92
- <div class="cpub-image-fields">
93
- <input
94
- class="cpub-image-field"
95
- type="text"
96
- :value="alt"
97
- placeholder="Alt text (describes the image)..."
98
- aria-label="Image alt text"
99
- @input="updateField('alt', ($event.target as HTMLInputElement).value)"
100
- />
101
- <input
102
- class="cpub-image-field"
103
- type="text"
104
- :value="caption"
105
- placeholder="Caption (optional)..."
106
- aria-label="Image caption"
107
- @input="updateField('caption', ($event.target as HTMLInputElement).value)"
108
- />
109
- </div>
110
- </template>
111
- </div>
112
- </template>
113
-
114
- <style scoped>
115
- .cpub-image-block {
116
- display: flex;
117
- flex-direction: column;
118
- gap: 0;
119
- }
120
-
121
- .cpub-image-placeholder {
122
- display: flex;
123
- flex-direction: column;
124
- align-items: center;
125
- gap: 8px;
126
- padding: 36px 24px;
127
- border: 2px dashed var(--border2);
128
- background: var(--surface2);
129
- cursor: pointer;
130
- transition: all 0.12s;
131
- }
132
-
133
- .cpub-image-placeholder:hover {
134
- border-color: var(--accent);
135
- background: var(--accent-bg);
136
- }
137
-
138
- .cpub-image-placeholder-icon {
139
- font-size: 28px;
140
- color: var(--text-faint);
141
- }
142
-
143
- .cpub-image-placeholder:hover .cpub-image-placeholder-icon {
144
- color: var(--accent);
145
- }
146
-
147
- .cpub-image-placeholder-text {
148
- font-size: 12px;
149
- color: var(--text-dim);
150
- }
151
-
152
- .cpub-image-placeholder-hint {
153
- font-family: var(--font-mono);
154
- font-size: 10px;
155
- color: var(--text-faint);
156
- }
157
-
158
- .cpub-image-uploading {
159
- pointer-events: none;
160
- opacity: 0.7;
161
- }
162
-
163
- .cpub-image-error {
164
- padding: 6px 10px;
165
- font-size: 11px;
166
- color: var(--red);
167
- background: var(--red-bg);
168
- border: var(--border-width-default) solid var(--red-border);
169
- }
170
-
171
- .cpub-image-url-row {
172
- padding: 8px 0 0;
173
- }
174
-
175
- .cpub-image-url-input {
176
- width: 100%;
177
- padding: 6px 10px;
178
- font-size: 12px;
179
- background: var(--surface);
180
- border: var(--border-width-default) solid var(--border);
181
- color: var(--text);
182
- outline: none;
183
- }
184
-
185
- .cpub-image-url-input:focus {
186
- border-color: var(--accent);
187
- }
188
-
189
- .cpub-image-url-input::placeholder {
190
- color: var(--text-faint);
191
- }
192
-
193
- .cpub-image-preview {
194
- position: relative;
195
- }
196
-
197
- .cpub-image-preview-img {
198
- width: 100%;
199
- max-height: 500px;
200
- object-fit: contain;
201
- display: block;
202
- background: var(--surface2);
203
- border: var(--border-width-default) solid var(--border);
204
- }
205
-
206
- .cpub-image-remove {
207
- position: absolute;
208
- top: 8px;
209
- right: 8px;
210
- width: 28px;
211
- height: 28px;
212
- background: var(--text);
213
- border: none;
214
- color: var(--surface);
215
- font-size: 12px;
216
- cursor: pointer;
217
- display: flex;
218
- align-items: center;
219
- justify-content: center;
220
- opacity: 0;
221
- transition: opacity 0.12s;
222
- }
223
-
224
- .cpub-image-preview:hover .cpub-image-remove {
225
- opacity: 1;
226
- }
227
-
228
- .cpub-image-remove:hover {
229
- background: var(--red);
230
- }
231
-
232
- .cpub-image-fields {
233
- display: flex;
234
- flex-direction: column;
235
- gap: 4px;
236
- padding-top: 6px;
237
- }
238
-
239
- .cpub-image-field {
240
- width: 100%;
241
- padding: 5px 8px;
242
- font-size: 11px;
243
- background: var(--surface);
244
- border: var(--border-width-default) solid var(--border2);
245
- color: var(--text-dim);
246
- outline: none;
247
- font-style: italic;
248
- }
249
-
250
- .cpub-image-field:focus {
251
- border-color: var(--accent);
252
- color: var(--text);
253
- font-style: normal;
254
- }
255
-
256
- .cpub-image-field::placeholder {
257
- color: var(--text-faint);
258
- font-style: italic;
259
- }
260
-
261
- .cpub-sr-only {
262
- position: absolute;
263
- width: 1px;
264
- height: 1px;
265
- padding: 0;
266
- margin: -1px;
267
- overflow: hidden;
268
- clip: rect(0, 0, 0, 0);
269
- border: 0;
270
- }
271
- </style>