@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,258 +0,0 @@
1
- <script setup lang="ts">
2
- import { markdownToBlockTuples } from '@commonpub/editor';
3
- import type { BlockTuple } from '@commonpub/editor';
4
-
5
- const props = defineProps<{
6
- content: { source: string };
7
- }>();
8
-
9
- const emit = defineEmits<{
10
- update: [content: { source: string }];
11
- }>();
12
-
13
- /** Type-safe block content accessors — cast content to record for dynamic key access */
14
- function bStr(block: BlockTuple, key: string): string {
15
- return String((block[1] as Record<string, unknown>)[key] ?? '');
16
- }
17
- function bNum(block: BlockTuple, key: string): number {
18
- return Number((block[1] as Record<string, unknown>)[key]) || 0;
19
- }
20
-
21
- const viewMode = ref<'edit' | 'split' | 'preview'>('split');
22
- const source = ref(props.content.source || '');
23
-
24
- watch(() => props.content.source, (val) => {
25
- if (val !== source.value) source.value = val;
26
- });
27
-
28
- const previewBlocks = computed(() => {
29
- if (!source.value.trim()) return [];
30
- try {
31
- return markdownToBlockTuples(source.value);
32
- } catch {
33
- return [];
34
- }
35
- });
36
-
37
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
38
- function handleInput(): void {
39
- if (debounceTimer) clearTimeout(debounceTimer);
40
- debounceTimer = setTimeout(() => {
41
- emit('update', { source: source.value });
42
- }, 300);
43
- }
44
-
45
- onUnmounted(() => {
46
- if (debounceTimer) clearTimeout(debounceTimer);
47
- });
48
- </script>
49
-
50
- <template>
51
- <div class="md-block" :class="[`md-block--${viewMode}`]">
52
- <div class="md-block-toolbar">
53
- <span class="md-block-label"><i class="fa-brands fa-markdown"></i> Markdown</span>
54
- <div class="md-block-modes">
55
- <button :class="{ active: viewMode === 'edit' }" @click="viewMode = 'edit'" title="Edit">
56
- <i class="fa-solid fa-code"></i>
57
- </button>
58
- <button :class="{ active: viewMode === 'split' }" @click="viewMode = 'split'" title="Split">
59
- <i class="fa-solid fa-columns"></i>
60
- </button>
61
- <button :class="{ active: viewMode === 'preview' }" @click="viewMode = 'preview'" title="Preview">
62
- <i class="fa-solid fa-eye"></i>
63
- </button>
64
- </div>
65
- </div>
66
-
67
- <div class="md-block-content">
68
- <div v-if="viewMode !== 'preview'" class="md-block-editor">
69
- <textarea
70
- v-model="source"
71
- class="md-block-textarea"
72
- placeholder="Write markdown here..."
73
- spellcheck="false"
74
- @input="handleInput"
75
- />
76
- </div>
77
-
78
- <div v-if="viewMode !== 'edit'" class="md-block-preview">
79
- <div v-if="!previewBlocks.length" class="md-block-empty">
80
- <i class="fa-solid fa-file-lines"></i>
81
- <span>Preview will appear here</span>
82
- </div>
83
- <div v-else class="md-block-preview-blocks">
84
- <div v-for="(block, i) in previewBlocks" :key="i" class="md-preview-block">
85
- <template v-if="block[0] === 'heading'">
86
- <component :is="`h${bNum(block, 'level')}`" class="md-preview-heading">
87
- {{ bStr(block, 'text') }}
88
- </component>
89
- </template>
90
- <template v-else-if="block[0] === 'text'">
91
- <div class="md-preview-text" v-html="bStr(block, 'html')" />
92
- </template>
93
- <template v-else-if="block[0] === 'code'">
94
- <pre class="md-preview-code"><code>{{ bStr(block, 'code') }}</code></pre>
95
- </template>
96
- <template v-else-if="block[0] === 'image'">
97
- <figure class="md-preview-figure">
98
- <img :src="bStr(block, 'src')" :alt="bStr(block, 'alt')" />
99
- </figure>
100
- </template>
101
- <template v-else-if="block[0] === 'callout'">
102
- <div :class="['md-preview-callout', `md-callout--${bStr(block, 'variant')}`]">
103
- <div v-html="bStr(block, 'html')" />
104
- </div>
105
- </template>
106
- <template v-else-if="block[0] === 'quote'">
107
- <blockquote class="md-preview-quote" v-html="bStr(block, 'html')" />
108
- </template>
109
- <template v-else-if="block[0] === 'divider'">
110
- <hr class="md-preview-hr" />
111
- </template>
112
- <template v-else>
113
- <div class="md-preview-unknown">{{ block[0] }} block</div>
114
- </template>
115
- </div>
116
- </div>
117
- </div>
118
- </div>
119
- </div>
120
- </template>
121
-
122
- <style scoped>
123
- .md-block {
124
- border: var(--border-width-default) solid var(--border);
125
- border-radius: 0;
126
- background: var(--surface);
127
- overflow: hidden;
128
- }
129
-
130
- .md-block-toolbar {
131
- display: flex;
132
- align-items: center;
133
- justify-content: space-between;
134
- padding: 6px 12px;
135
- background: var(--surface2);
136
- border-bottom: var(--border-width-default) solid var(--border);
137
- }
138
-
139
- .md-block-label {
140
- font-size: 11px;
141
- font-weight: 600;
142
- color: var(--text-dim);
143
- display: flex;
144
- align-items: center;
145
- gap: 6px;
146
- }
147
-
148
- .md-block-modes {
149
- display: flex;
150
- gap: 2px;
151
- background: var(--surface3);
152
- border-radius: 0;
153
- padding: 2px;
154
- }
155
-
156
- .md-block-modes button {
157
- padding: 3px 8px;
158
- border: none;
159
- border-radius: 0;
160
- background: none;
161
- color: var(--text-faint);
162
- cursor: pointer;
163
- font-size: 11px;
164
- }
165
-
166
- .md-block-modes button.active {
167
- background: var(--surface);
168
- color: var(--text);
169
- box-shadow: var(--shadow-sm);
170
- }
171
-
172
- .md-block-content {
173
- display: flex;
174
- min-height: 200px;
175
- }
176
-
177
- .md-block--edit .md-block-editor { flex: 1; }
178
- .md-block--preview .md-block-preview { flex: 1; }
179
- .md-block--split .md-block-editor { flex: 1; border-right: var(--border-width-default) solid var(--border); }
180
- .md-block--split .md-block-preview { flex: 1; }
181
-
182
- .md-block-textarea {
183
- width: 100%;
184
- height: 100%;
185
- min-height: 200px;
186
- resize: vertical;
187
- border: none;
188
- outline: none;
189
- padding: 12px;
190
- font-family: var(--font-mono);
191
- font-size: 12px;
192
- line-height: 1.6;
193
- color: var(--text);
194
- background: var(--bg);
195
- tab-size: 2;
196
- }
197
-
198
- .md-block-preview {
199
- padding: 12px;
200
- font-size: 13px;
201
- line-height: 1.6;
202
- color: var(--text);
203
- overflow-y: auto;
204
- }
205
-
206
- .md-block-empty {
207
- display: flex;
208
- flex-direction: column;
209
- align-items: center;
210
- justify-content: center;
211
- height: 100%;
212
- min-height: 200px;
213
- gap: 8px;
214
- color: var(--text-faint);
215
- font-size: 12px;
216
- }
217
-
218
- .md-preview-heading { margin-bottom: 8px; }
219
- .md-preview-text { margin-bottom: 8px; }
220
- .md-preview-code {
221
- background: var(--surface2);
222
- border: var(--border-width-default) solid var(--border);
223
- border-radius: 0;
224
- padding: 10px;
225
- font-family: var(--font-mono);
226
- font-size: 11px;
227
- overflow-x: auto;
228
- margin-bottom: 8px;
229
- }
230
- .md-preview-figure img { max-width: 100%; border-radius: 0; }
231
- .md-preview-callout {
232
- padding: 10px 12px;
233
- border-radius: 0;
234
- border-left: 3px solid;
235
- margin-bottom: 8px;
236
- font-size: 12px;
237
- }
238
- .md-callout--info { background: var(--teal-bg); border-color: var(--teal); }
239
- .md-callout--tip { background: var(--green-bg); border-color: var(--green); }
240
- .md-callout--warning { background: var(--yellow-bg); border-color: var(--yellow); }
241
- .md-callout--danger { background: var(--red-bg); border-color: var(--red); }
242
- .md-preview-quote {
243
- border-left: 3px solid var(--border);
244
- padding-left: 12px;
245
- color: var(--text-dim);
246
- font-style: italic;
247
- margin-bottom: 8px;
248
- }
249
- .md-preview-hr { border: none; border-top: var(--border-width-default) solid var(--border); margin: 12px 0; }
250
- .md-preview-unknown {
251
- padding: 6px 10px;
252
- background: var(--surface2);
253
- border-radius: 0;
254
- font-size: 11px;
255
- color: var(--text-faint);
256
- margin-bottom: 8px;
257
- }
258
- </style>
@@ -1,37 +0,0 @@
1
- <script setup lang="ts">
2
- const props = defineProps<{ content: Record<string, unknown> }>();
3
- const emit = defineEmits<{ update: [content: Record<string, unknown>] }>();
4
-
5
- const expression = computed(() => (props.content.expression as string) ?? '');
6
- const notation = computed(() => (props.content.notation as string) ?? 'latex');
7
-
8
- function updateExpression(value: string): void {
9
- emit('update', { ...props.content, expression: value });
10
- }
11
- </script>
12
-
13
- <template>
14
- <div class="cpub-math-edit">
15
- <div class="cpub-math-edit-header"><i class="fa-solid fa-square-root-variable"></i> Math Notation</div>
16
- <div class="cpub-math-edit-body">
17
- <textarea
18
- class="cpub-math-textarea"
19
- :value="expression"
20
- placeholder="e.g. f(x) = \sum_{i=1}^{n} w_i \cdot x_i + b"
21
- rows="3"
22
- @input="updateExpression(($event.target as HTMLTextAreaElement).value)"
23
- />
24
- <div class="cpub-math-hint">Enter LaTeX or plain math notation</div>
25
- </div>
26
- </div>
27
- </template>
28
-
29
- <style scoped>
30
- .cpub-math-edit { border: var(--border-width-default) solid var(--border2); background: var(--surface); }
31
- .cpub-math-edit-header { padding: 8px 12px; font-size: 12px; font-weight: 600; background: var(--surface2); border-bottom: var(--border-width-default) solid var(--border2); display: flex; align-items: center; gap: 8px; }
32
- .cpub-math-edit-header i { color: var(--purple); }
33
- .cpub-math-edit-body { padding: 12px; }
34
- .cpub-math-textarea { width: 100%; font-family: var(--font-mono); font-size: 13px; background: var(--surface2); border: var(--border-width-default) solid var(--border2); padding: 8px; color: var(--text); outline: none; resize: vertical; }
35
- .cpub-math-textarea:focus { border-color: var(--accent); }
36
- .cpub-math-hint { font-size: 10px; color: var(--text-faint); margin-top: 4px; }
37
- </style>
@@ -1,358 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Parts list block — editable BOM table with product catalog autocomplete.
4
- * When a user selects a product from the catalog, the productId is stored
5
- * so the content-products join table can be synced on save (BOM → product hub gallery).
6
- */
7
- interface Part {
8
- name: string;
9
- qty: number;
10
- price?: number;
11
- url?: string;
12
- category?: string;
13
- required?: boolean;
14
- notes?: string;
15
- productId?: string;
16
- productSlug?: string;
17
- }
18
-
19
- interface ProductResult {
20
- id: string;
21
- name: string;
22
- slug: string;
23
- description: string | null;
24
- category: string | null;
25
- imageUrl: string | null;
26
- purchaseUrl: string | null;
27
- }
28
-
29
- const props = defineProps<{
30
- content: Record<string, unknown>;
31
- }>();
32
-
33
- const emit = defineEmits<{
34
- update: [content: Record<string, unknown>];
35
- }>();
36
-
37
- const parts = computed(() => (props.content.parts as Part[]) ?? []);
38
-
39
- function updatePart(index: number, field: string, value: unknown): void {
40
- const updated = [...parts.value];
41
- updated[index] = { ...updated[index]!, [field]: value };
42
- emit('update', { parts: updated });
43
- }
44
-
45
- function addPart(): void {
46
- emit('update', { parts: [...parts.value, { name: '', qty: 1, required: true }] });
47
- }
48
-
49
- function removePart(index: number): void {
50
- const updated = parts.value.filter((_: Part, i: number) => i !== index);
51
- emit('update', { parts: updated });
52
- }
53
-
54
- function selectProduct(index: number, product: ProductResult): void {
55
- const updated = [...parts.value];
56
- updated[index] = {
57
- ...updated[index]!,
58
- name: product.name,
59
- url: product.purchaseUrl ?? undefined,
60
- productId: product.id,
61
- productSlug: product.slug,
62
- category: product.category ?? undefined,
63
- };
64
- emit('update', { parts: updated });
65
- // Close autocomplete
66
- activeAutocomplete.value = -1;
67
- autocompleteResults.value = [];
68
- }
69
-
70
- function clearProductLink(index: number): void {
71
- const updated = [...parts.value];
72
- updated[index] = { ...updated[index]!, productId: undefined, productSlug: undefined };
73
- emit('update', { parts: updated });
74
- }
75
-
76
- const totalPrice = computed(() => {
77
- return parts.value.reduce((sum: number, p: Part) => sum + (p.price ?? 0) * p.qty, 0);
78
- });
79
-
80
- // --- Autocomplete ---
81
- const activeAutocomplete = ref(-1);
82
- const autocompleteResults = ref<ProductResult[]>([]);
83
- const autocompleteLoading = ref(false);
84
- const debounceTimer = ref<ReturnType<typeof setTimeout> | null>(null);
85
-
86
- function onNameInput(index: number, value: string): void {
87
- updatePart(index, 'name', value);
88
-
89
- // Clear product link when name changes manually
90
- if (parts.value[index]?.productId) {
91
- clearProductLink(index);
92
- }
93
-
94
- if (debounceTimer.value) clearTimeout(debounceTimer.value);
95
-
96
- if (value.length < 2) {
97
- autocompleteResults.value = [];
98
- activeAutocomplete.value = -1;
99
- return;
100
- }
101
-
102
- activeAutocomplete.value = index;
103
- debounceTimer.value = setTimeout(async () => {
104
- autocompleteLoading.value = true;
105
- try {
106
- const data = await $fetch<{ items: ProductResult[] }>('/api/products', {
107
- query: { q: value, limit: 5 },
108
- });
109
- autocompleteResults.value = data.items;
110
- } catch {
111
- autocompleteResults.value = [];
112
- } finally {
113
- autocompleteLoading.value = false;
114
- }
115
- }, 250);
116
- }
117
-
118
- function onNameBlur(): void {
119
- // Delay to allow click on autocomplete item
120
- setTimeout(() => {
121
- activeAutocomplete.value = -1;
122
- autocompleteResults.value = [];
123
- }, 200);
124
- }
125
- </script>
126
-
127
- <template>
128
- <div class="cpub-parts-block">
129
- <div class="cpub-parts-header">
130
- <div class="cpub-parts-icon"><i class="fa-solid fa-list-check"></i></div>
131
- <span class="cpub-parts-title">Parts List</span>
132
- <span class="cpub-parts-count">{{ parts.length }} items<template v-if="totalPrice > 0"> · ${{ totalPrice.toFixed(2) }} est.</template></span>
133
- <button class="cpub-parts-add-btn" @click="addPart">
134
- <i class="fa-solid fa-plus"></i> Add part
135
- </button>
136
- </div>
137
- <table v-if="parts.length > 0" class="cpub-parts-table">
138
- <thead>
139
- <tr>
140
- <th>Part</th>
141
- <th class="cpub-parts-qty">Qty</th>
142
- <th>Notes</th>
143
- <th class="cpub-parts-price">Price</th>
144
- <th class="cpub-parts-actions"></th>
145
- </tr>
146
- </thead>
147
- <tbody>
148
- <tr v-for="(part, i) in parts" :key="i">
149
- <td class="cpub-parts-name-cell">
150
- <div class="cpub-parts-name-wrap">
151
- <input
152
- class="cpub-parts-input"
153
- type="text"
154
- :value="part.name"
155
- placeholder="Search product catalog..."
156
- :aria-label="`Part ${i + 1} name`"
157
- @input="onNameInput(i, ($event.target as HTMLInputElement).value)"
158
- @blur="onNameBlur"
159
- />
160
- <span v-if="part.productId" class="cpub-parts-linked" title="Linked to product catalog">
161
- <i class="fa-solid fa-link"></i>
162
- </span>
163
- </div>
164
- <!-- Autocomplete dropdown -->
165
- <div v-if="activeAutocomplete === i && autocompleteResults.length > 0" class="cpub-parts-ac">
166
- <button
167
- v-for="product in autocompleteResults"
168
- :key="product.id"
169
- class="cpub-parts-ac-item"
170
- @mousedown.prevent="selectProduct(i, product)"
171
- >
172
- <div class="cpub-parts-ac-icon">
173
- <img v-if="product.imageUrl" :src="product.imageUrl" :alt="product.name" />
174
- <i v-else class="fa-solid fa-microchip"></i>
175
- </div>
176
- <div class="cpub-parts-ac-info">
177
- <span class="cpub-parts-ac-name">{{ product.name }}</span>
178
- <span v-if="product.category" class="cpub-parts-ac-cat">{{ product.category }}</span>
179
- </div>
180
- </button>
181
- </div>
182
- <div v-else-if="activeAutocomplete === i && autocompleteLoading" class="cpub-parts-ac">
183
- <div class="cpub-parts-ac-loading">Searching...</div>
184
- </div>
185
- </td>
186
- <td class="cpub-parts-qty">
187
- <input class="cpub-parts-input cpub-parts-input-sm" type="number" :value="part.qty" min="1" :aria-label="`Part ${i + 1} quantity`" @input="updatePart(i, 'qty', Number(($event.target as HTMLInputElement).value))" />
188
- </td>
189
- <td>
190
- <input class="cpub-parts-input" type="text" :value="part.notes ?? ''" placeholder="Notes..." :aria-label="`Part ${i + 1} notes`" @input="updatePart(i, 'notes', ($event.target as HTMLInputElement).value)" />
191
- </td>
192
- <td class="cpub-parts-price">
193
- <input class="cpub-parts-input cpub-parts-input-sm" type="number" step="0.01" :value="part.price ?? ''" placeholder="0.00" :aria-label="`Part ${i + 1} price`" @input="updatePart(i, 'price', Number(($event.target as HTMLInputElement).value))" />
194
- </td>
195
- <td class="cpub-parts-actions">
196
- <button class="cpub-parts-remove" title="Remove" @click="removePart(i)">
197
- <i class="fa-solid fa-xmark"></i>
198
- </button>
199
- </td>
200
- </tr>
201
- </tbody>
202
- </table>
203
- <div v-else class="cpub-parts-empty" @click="addPart">
204
- <i class="fa-solid fa-plus"></i> Click to add your first part
205
- </div>
206
- </div>
207
- </template>
208
-
209
- <style scoped>
210
- .cpub-parts-block { border: var(--border-width-default) solid var(--border2); background: var(--surface); }
211
-
212
- .cpub-parts-header {
213
- display: flex; align-items: center; gap: 8px;
214
- padding: 10px 14px;
215
- border-bottom: var(--border-width-default) solid var(--border2);
216
- background: var(--surface2);
217
- }
218
-
219
- .cpub-parts-icon { font-size: 12px; color: var(--accent); }
220
- .cpub-parts-title { font-size: 12px; font-weight: 600; }
221
- .cpub-parts-count { font-family: var(--font-mono); font-size: 10px; color: var(--text-faint); margin-left: auto; }
222
-
223
- .cpub-parts-add-btn {
224
- font-family: var(--font-mono); font-size: 10px;
225
- padding: 3px 8px; background: transparent;
226
- border: var(--border-width-default) solid var(--border2); color: var(--text-dim);
227
- cursor: pointer; display: flex; align-items: center; gap: 4px;
228
- margin-left: 8px;
229
- }
230
- .cpub-parts-add-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
231
-
232
- .cpub-parts-table { width: 100%; border-collapse: collapse; }
233
- .cpub-parts-table th {
234
- font-family: var(--font-mono); font-size: 9px; font-weight: 700;
235
- text-transform: uppercase; letter-spacing: 0.1em;
236
- text-align: left; padding: 6px 10px;
237
- background: var(--text); color: var(--surface);
238
- }
239
- .cpub-parts-table td { padding: 4px 6px; border-bottom: var(--border-width-default) solid var(--border2); }
240
- .cpub-parts-qty { width: 50px; text-align: center; }
241
- .cpub-parts-price { width: 80px; }
242
- .cpub-parts-actions { width: 30px; text-align: center; }
243
-
244
- .cpub-parts-name-cell {
245
- position: relative;
246
- }
247
-
248
- .cpub-parts-name-wrap {
249
- display: flex;
250
- align-items: center;
251
- gap: 4px;
252
- }
253
-
254
- .cpub-parts-linked {
255
- font-size: 9px;
256
- color: var(--accent);
257
- flex-shrink: 0;
258
- }
259
-
260
- .cpub-parts-input {
261
- width: 100%; padding: 4px 6px; font-size: 12px;
262
- background: transparent; border: var(--border-width-default) solid transparent;
263
- color: var(--text); outline: none;
264
- }
265
- .cpub-parts-input:focus { border-color: var(--accent); background: var(--accent-bg); }
266
- .cpub-parts-input::placeholder { color: var(--text-faint); }
267
- .cpub-parts-input-sm { width: 60px; text-align: center; }
268
-
269
- .cpub-parts-remove {
270
- background: none; border: none; color: var(--text-faint);
271
- cursor: pointer; font-size: 10px; padding: 4px;
272
- }
273
- .cpub-parts-remove:hover { color: var(--red); }
274
-
275
- .cpub-parts-empty {
276
- padding: 24px; text-align: center;
277
- font-size: 12px; color: var(--text-faint);
278
- cursor: pointer;
279
- }
280
- .cpub-parts-empty:hover { color: var(--accent); background: var(--accent-bg); }
281
-
282
- /* Autocomplete dropdown */
283
- .cpub-parts-ac {
284
- position: absolute;
285
- top: 100%;
286
- left: 0;
287
- right: 0;
288
- z-index: 50;
289
- background: var(--surface);
290
- border: var(--border-width-default) solid var(--border);
291
- box-shadow: var(--shadow-md);
292
- max-height: 200px;
293
- overflow-y: auto;
294
- }
295
-
296
- .cpub-parts-ac-item {
297
- display: flex;
298
- align-items: center;
299
- gap: 8px;
300
- width: 100%;
301
- padding: 6px 10px;
302
- border: none;
303
- background: transparent;
304
- color: var(--text);
305
- cursor: pointer;
306
- text-align: left;
307
- font-size: 12px;
308
- }
309
-
310
- .cpub-parts-ac-item:hover {
311
- background: var(--accent-bg);
312
- }
313
-
314
- .cpub-parts-ac-icon {
315
- width: 24px;
316
- height: 24px;
317
- background: var(--surface2);
318
- border: var(--border-width-default) solid var(--border2);
319
- display: flex;
320
- align-items: center;
321
- justify-content: center;
322
- font-size: 10px;
323
- color: var(--text-faint);
324
- flex-shrink: 0;
325
- }
326
-
327
- .cpub-parts-ac-icon img {
328
- width: 100%;
329
- height: 100%;
330
- object-fit: cover;
331
- }
332
-
333
- .cpub-parts-ac-info {
334
- flex: 1;
335
- min-width: 0;
336
- display: flex;
337
- align-items: center;
338
- gap: 6px;
339
- }
340
-
341
- .cpub-parts-ac-name {
342
- font-weight: 500;
343
- }
344
-
345
- .cpub-parts-ac-cat {
346
- font-size: 9px;
347
- font-family: var(--font-mono);
348
- color: var(--text-faint);
349
- text-transform: uppercase;
350
- }
351
-
352
- .cpub-parts-ac-loading {
353
- padding: 10px;
354
- text-align: center;
355
- font-size: 11px;
356
- color: var(--text-faint);
357
- }
358
- </style>
@@ -1,47 +0,0 @@
1
- <script setup lang="ts">
2
- const props = defineProps<{ content: Record<string, unknown> }>();
3
- const emit = defineEmits<{ update: [content: Record<string, unknown>] }>();
4
- interface Option { text: string; correct: boolean; }
5
- const question = computed(() => (props.content.question as string) ?? '');
6
- const options = computed(() => (props.content.options as Option[]) ?? []);
7
- function updateQuestion(value: string): void { emit('update', { ...props.content, question: value }); }
8
- function addOption(): void { emit('update', { ...props.content, options: [...options.value, { text: '', correct: false }] }); }
9
- function updateOption(i: number, field: string, value: unknown): void {
10
- const updated = [...options.value];
11
- updated[i] = { ...updated[i]!, [field]: value };
12
- emit('update', { ...props.content, options: updated });
13
- }
14
- function removeOption(i: number): void {
15
- emit('update', { ...props.content, options: options.value.filter((_: Option, idx: number) => idx !== i) });
16
- }
17
- </script>
18
- <template>
19
- <div class="cpub-quiz-block">
20
- <div class="cpub-quiz-header"><i class="fa-solid fa-circle-question"></i> Quiz</div>
21
- <div class="cpub-quiz-body">
22
- <input class="cpub-quiz-question" type="text" :value="question" placeholder="Ask a question..." @input="updateQuestion(($event.target as HTMLInputElement).value)" />
23
- <div v-for="(opt, i) in options" :key="i" class="cpub-quiz-option">
24
- <input type="checkbox" :checked="opt.correct" @change="updateOption(i, 'correct', ($event.target as HTMLInputElement).checked)" />
25
- <input class="cpub-quiz-opt-text" type="text" :value="opt.text" placeholder="Option text..." @input="updateOption(i, 'text', ($event.target as HTMLInputElement).value)" />
26
- <button class="cpub-quiz-opt-remove" @click="removeOption(i)"><i class="fa-solid fa-xmark"></i></button>
27
- </div>
28
- <button class="cpub-quiz-add-opt" @click="addOption"><i class="fa-solid fa-plus"></i> Add option</button>
29
- </div>
30
- </div>
31
- </template>
32
- <style scoped>
33
- .cpub-quiz-block { border: var(--border-width-default) solid var(--border2); background: var(--surface); }
34
- .cpub-quiz-header { padding: 8px 12px; font-size: 12px; font-weight: 600; background: var(--surface2); border-bottom: var(--border-width-default) solid var(--border2); display: flex; align-items: center; gap: 8px; }
35
- .cpub-quiz-header i { color: var(--purple); }
36
- .cpub-quiz-body { padding: 12px; }
37
- .cpub-quiz-question { width: 100%; font-size: 14px; font-weight: 600; background: transparent; border: none; border-bottom: var(--border-width-default) solid var(--border2); padding: 6px 0; outline: none; color: var(--text); margin-bottom: 12px; }
38
- .cpub-quiz-question::placeholder { color: var(--text-faint); }
39
- .cpub-quiz-option { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
40
- .cpub-quiz-option input[type="checkbox"] { accent-color: var(--green); }
41
- .cpub-quiz-opt-text { flex: 1; font-size: 12px; background: transparent; border: none; outline: none; color: var(--text); padding: 4px; }
42
- .cpub-quiz-opt-text::placeholder { color: var(--text-faint); }
43
- .cpub-quiz-opt-remove { background: none; border: none; color: var(--text-faint); cursor: pointer; }
44
- .cpub-quiz-opt-remove:hover { color: var(--red); }
45
- .cpub-quiz-add-opt { margin-top: 8px; font-size: 11px; color: var(--text-dim); background: none; border: 2px dashed var(--border2); padding: 6px 12px; cursor: pointer; width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px; }
46
- .cpub-quiz-add-opt:hover { border-color: var(--accent); color: var(--accent); }
47
- </style>