@commonpub/layer 0.7.2 → 0.7.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.
Files changed (38) hide show
  1. package/components/editors/ArticleEditor.vue +11 -12
  2. package/components/editors/BlogEditor.vue +17 -18
  3. package/components/editors/ExplainerEditor.vue +13 -14
  4. package/components/editors/ProjectEditor.vue +17 -18
  5. package/composables/useMarkdownImport.ts +1 -1
  6. package/package.json +5 -5
  7. package/pages/docs/[siteSlug]/edit.vue +4 -4
  8. package/pages/u/[username]/[type]/[slug]/edit.vue +2 -1
  9. package/components/editors/BlockCanvas.vue +0 -487
  10. package/components/editors/BlockInsertZone.vue +0 -84
  11. package/components/editors/BlockPicker.vue +0 -285
  12. package/components/editors/BlockWrapper.vue +0 -192
  13. package/components/editors/EditorBlocks.vue +0 -248
  14. package/components/editors/EditorSection.vue +0 -81
  15. package/components/editors/EditorShell.vue +0 -196
  16. package/components/editors/EditorTagInput.vue +0 -114
  17. package/components/editors/EditorVisibility.vue +0 -110
  18. package/components/editors/blocks/BuildStepBlock.vue +0 -102
  19. package/components/editors/blocks/CalloutBlock.vue +0 -122
  20. package/components/editors/blocks/CheckpointBlock.vue +0 -27
  21. package/components/editors/blocks/CodeBlock.vue +0 -177
  22. package/components/editors/blocks/DividerBlock.vue +0 -22
  23. package/components/editors/blocks/DownloadsBlock.vue +0 -41
  24. package/components/editors/blocks/EmbedBlock.vue +0 -20
  25. package/components/editors/blocks/GalleryBlock.vue +0 -236
  26. package/components/editors/blocks/HeadingBlock.vue +0 -96
  27. package/components/editors/blocks/ImageBlock.vue +0 -271
  28. package/components/editors/blocks/MarkdownBlock.vue +0 -258
  29. package/components/editors/blocks/MathBlock.vue +0 -37
  30. package/components/editors/blocks/PartsListBlock.vue +0 -358
  31. package/components/editors/blocks/QuizBlock.vue +0 -47
  32. package/components/editors/blocks/QuoteBlock.vue +0 -101
  33. package/components/editors/blocks/SectionHeaderBlock.vue +0 -130
  34. package/components/editors/blocks/SliderBlock.vue +0 -318
  35. package/components/editors/blocks/TextBlock.vue +0 -201
  36. package/components/editors/blocks/ToolListBlock.vue +0 -70
  37. package/components/editors/blocks/VideoBlock.vue +0 -22
  38. 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>