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