@commonpub/layer 0.7.1 → 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 (43) 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 +8 -8
  7. package/pages/docs/[siteSlug]/edit.vue +4 -4
  8. package/pages/federated-hubs/[id]/posts/[postId].vue +91 -13
  9. package/pages/hubs/[slug]/index.vue +1 -1
  10. package/pages/hubs/[slug]/posts/[postId].vue +12 -0
  11. package/pages/u/[username]/[type]/[slug]/edit.vue +2 -1
  12. package/server/api/federated-hubs/[id]/posts/[postId]/replies.get.ts +13 -0
  13. package/server/api/federation/hub-post-reply.post.ts +16 -10
  14. package/components/editors/BlockCanvas.vue +0 -487
  15. package/components/editors/BlockInsertZone.vue +0 -84
  16. package/components/editors/BlockPicker.vue +0 -285
  17. package/components/editors/BlockWrapper.vue +0 -192
  18. package/components/editors/EditorBlocks.vue +0 -248
  19. package/components/editors/EditorSection.vue +0 -81
  20. package/components/editors/EditorShell.vue +0 -196
  21. package/components/editors/EditorTagInput.vue +0 -114
  22. package/components/editors/EditorVisibility.vue +0 -110
  23. package/components/editors/blocks/BuildStepBlock.vue +0 -102
  24. package/components/editors/blocks/CalloutBlock.vue +0 -122
  25. package/components/editors/blocks/CheckpointBlock.vue +0 -27
  26. package/components/editors/blocks/CodeBlock.vue +0 -177
  27. package/components/editors/blocks/DividerBlock.vue +0 -22
  28. package/components/editors/blocks/DownloadsBlock.vue +0 -41
  29. package/components/editors/blocks/EmbedBlock.vue +0 -20
  30. package/components/editors/blocks/GalleryBlock.vue +0 -236
  31. package/components/editors/blocks/HeadingBlock.vue +0 -96
  32. package/components/editors/blocks/ImageBlock.vue +0 -271
  33. package/components/editors/blocks/MarkdownBlock.vue +0 -258
  34. package/components/editors/blocks/MathBlock.vue +0 -37
  35. package/components/editors/blocks/PartsListBlock.vue +0 -358
  36. package/components/editors/blocks/QuizBlock.vue +0 -47
  37. package/components/editors/blocks/QuoteBlock.vue +0 -101
  38. package/components/editors/blocks/SectionHeaderBlock.vue +0 -130
  39. package/components/editors/blocks/SliderBlock.vue +0 -318
  40. package/components/editors/blocks/TextBlock.vue +0 -201
  41. package/components/editors/blocks/ToolListBlock.vue +0 -70
  42. package/components/editors/blocks/VideoBlock.vue +0 -22
  43. package/composables/useBlockEditor.ts +0 -187
@@ -1,101 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Blockquote block — styled quote with editable body and attribution.
4
- */
5
- import { sanitizeBlockHtml } from '../../../composables/useSanitize';
6
-
7
- const props = defineProps<{
8
- content: Record<string, unknown>;
9
- }>();
10
-
11
- const emit = defineEmits<{
12
- update: [content: Record<string, unknown>];
13
- }>();
14
-
15
- const html = computed(() => (props.content.html as string) ?? '');
16
- const attribution = computed(() => (props.content.attribution as string) ?? '');
17
-
18
- function onBodyInput(event: Event): void {
19
- const el = event.target as HTMLElement;
20
- emit('update', { html: sanitizeBlockHtml(el.innerHTML), attribution: attribution.value });
21
- }
22
-
23
- function onAttributionInput(event: Event): void {
24
- emit('update', { html: html.value, attribution: (event.target as HTMLInputElement).value });
25
- }
26
- </script>
27
-
28
- <template>
29
- <div class="cpub-quote-block">
30
- <div class="cpub-quote-bar" aria-hidden="true" />
31
- <div class="cpub-quote-body">
32
- <div
33
- class="cpub-quote-text"
34
- contenteditable="true"
35
- data-placeholder="Enter quote..."
36
- @input="onBodyInput"
37
- v-html="html"
38
- />
39
- <input
40
- class="cpub-quote-attribution"
41
- type="text"
42
- :value="attribution"
43
- placeholder="— Source or author"
44
- aria-label="Quote attribution"
45
- @input="onAttributionInput"
46
- />
47
- </div>
48
- </div>
49
- </template>
50
-
51
- <style scoped>
52
- .cpub-quote-block {
53
- display: flex;
54
- gap: 0;
55
- background: var(--surface2);
56
- border: var(--border-width-default) solid var(--border2);
57
- }
58
-
59
- .cpub-quote-bar {
60
- width: 5px;
61
- background: var(--accent);
62
- flex-shrink: 0;
63
- }
64
-
65
- .cpub-quote-body {
66
- flex: 1;
67
- padding: 16px 20px;
68
- }
69
-
70
- .cpub-quote-text {
71
- font-size: 16px;
72
- font-style: italic;
73
- line-height: 1.7;
74
- color: var(--text);
75
- outline: none;
76
- min-height: 1.7em;
77
- }
78
-
79
- .cpub-quote-text:empty::before {
80
- content: attr(data-placeholder);
81
- color: var(--text-faint);
82
- pointer-events: none;
83
- }
84
-
85
- .cpub-quote-attribution {
86
- display: block;
87
- width: 100%;
88
- margin-top: 10px;
89
- padding: 0;
90
- font-family: var(--font-mono);
91
- font-size: 11px;
92
- color: var(--text-dim);
93
- background: transparent;
94
- border: none;
95
- outline: none;
96
- }
97
-
98
- .cpub-quote-attribution::placeholder {
99
- color: var(--text-faint);
100
- }
101
- </style>
@@ -1,130 +0,0 @@
1
- <script setup lang="ts">
2
- const props = defineProps<{
3
- content: Record<string, unknown>;
4
- }>();
5
-
6
- const emit = defineEmits<{
7
- update: [content: Record<string, unknown>];
8
- }>();
9
-
10
- const tag = computed({
11
- get: () => (props.content.tag as string) || '',
12
- set: (v: string) => emit('update', { ...props.content, tag: v }),
13
- });
14
-
15
- const title = computed({
16
- get: () => (props.content.title as string) || '',
17
- set: (v: string) => emit('update', { ...props.content, title: v }),
18
- });
19
-
20
- const body = computed({
21
- get: () => (props.content.body as string) || '',
22
- set: (v: string) => emit('update', { ...props.content, body: v }),
23
- });
24
- </script>
25
-
26
- <template>
27
- <div class="cpub-section-header-block">
28
- <input
29
- v-model="tag"
30
- type="text"
31
- class="cpub-shb-tag"
32
- placeholder="§ 01 — Section Name"
33
- aria-label="Section tag"
34
- />
35
- <input
36
- v-model="title"
37
- type="text"
38
- class="cpub-shb-title"
39
- placeholder="Section title"
40
- aria-label="Section title"
41
- />
42
- <textarea
43
- v-model="body"
44
- class="cpub-shb-body"
45
- placeholder="Brief intro for this section..."
46
- rows="2"
47
- aria-label="Section intro"
48
- />
49
- </div>
50
- </template>
51
-
52
- <style scoped>
53
- .cpub-section-header-block {
54
- padding: 28px 24px 24px;
55
- }
56
-
57
- .cpub-shb-tag {
58
- font-family: var(--font-mono);
59
- font-size: 9px;
60
- font-weight: 700;
61
- letter-spacing: 0.2em;
62
- text-transform: uppercase;
63
- color: var(--accent);
64
- margin-bottom: 10px;
65
- display: block;
66
- width: 100%;
67
- background: none;
68
- border: var(--border-width-default) solid transparent;
69
- padding: 2px 4px;
70
- outline: none;
71
- }
72
-
73
- .cpub-shb-tag::placeholder {
74
- color: var(--text-faint);
75
- font-weight: 500;
76
- }
77
-
78
- .cpub-shb-tag:focus {
79
- border-color: var(--accent-border);
80
- background: var(--accent-bg);
81
- }
82
-
83
- .cpub-shb-title {
84
- font-size: 26px;
85
- font-weight: 700;
86
- letter-spacing: -0.03em;
87
- color: var(--text);
88
- line-height: 1.15;
89
- margin-bottom: 10px;
90
- display: block;
91
- width: 100%;
92
- background: none;
93
- border: var(--border-width-default) solid transparent;
94
- padding: 4px;
95
- outline: none;
96
- font-family: inherit;
97
- }
98
-
99
- .cpub-shb-title::placeholder {
100
- color: var(--text-faint);
101
- }
102
-
103
- .cpub-shb-title:focus {
104
- border-color: var(--accent-border);
105
- background: var(--surface2);
106
- }
107
-
108
- .cpub-shb-body {
109
- font-size: 14px;
110
- color: var(--text-dim);
111
- line-height: 1.75;
112
- max-width: 540px;
113
- width: 100%;
114
- resize: vertical;
115
- background: none;
116
- border: var(--border-width-default) solid transparent;
117
- padding: 4px;
118
- outline: none;
119
- font-family: inherit;
120
- }
121
-
122
- .cpub-shb-body::placeholder {
123
- color: var(--text-faint);
124
- }
125
-
126
- .cpub-shb-body:focus {
127
- border-color: var(--accent-border);
128
- background: var(--surface2);
129
- }
130
- </style>
@@ -1,318 +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
- interface FeedbackRange {
6
- min: number;
7
- max: number;
8
- state: string;
9
- message: string;
10
- }
11
-
12
- const label = computed(() => (props.content.label as string) ?? '');
13
- const min = computed(() => (props.content.min as number) ?? 0);
14
- const max = computed(() => (props.content.max as number) ?? 100);
15
- const step = computed(() => (props.content.step as number) ?? 1);
16
- const unit = computed(() => (props.content.unit as string) ?? '');
17
- const defaultValue = computed(() => (props.content.defaultValue as number) ?? Math.round((min.value + max.value) / 2));
18
- const feedback = computed<FeedbackRange[]>(() => {
19
- const raw = props.content.feedback;
20
- if (!Array.isArray(raw)) return [];
21
- return raw as FeedbackRange[];
22
- });
23
-
24
- function update(field: string, value: unknown): void {
25
- emit('update', { ...props.content, [field]: value });
26
- }
27
-
28
- function addFeedbackRange(): void {
29
- const ranges = [...feedback.value];
30
- // Default: fill the remaining range
31
- const lastMax = ranges.length > 0 ? ranges[ranges.length - 1]!.max + 1 : min.value;
32
- ranges.push({
33
- min: lastMax,
34
- max: max.value,
35
- state: 'ok',
36
- message: 'Describe what this range means...',
37
- });
38
- update('feedback', ranges);
39
- }
40
-
41
- function updateFeedbackRange(index: number, field: keyof FeedbackRange, value: string | number): void {
42
- const ranges = [...feedback.value];
43
- const range = { ...ranges[index]! };
44
- if (field === 'min' || field === 'max') {
45
- range[field] = Number(value);
46
- } else {
47
- range[field] = value as string;
48
- }
49
- ranges[index] = range;
50
- update('feedback', ranges);
51
- }
52
-
53
- function removeFeedbackRange(index: number): void {
54
- const ranges = feedback.value.filter((_: FeedbackRange, i: number) => i !== index);
55
- update('feedback', ranges);
56
- }
57
-
58
- const stateOptions = [
59
- { value: 'low', label: 'Low', color: 'var(--yellow)' },
60
- { value: 'slow', label: 'Slow', color: 'var(--yellow)' },
61
- { value: 'ok', label: 'OK', color: 'var(--green)' },
62
- { value: 'good', label: 'Good', color: 'var(--green)' },
63
- { value: 'high', label: 'High', color: 'var(--red)' },
64
- { value: 'danger', label: 'Danger', color: 'var(--red)' },
65
- ];
66
-
67
- // Live preview value
68
- const previewValue = ref(defaultValue.value);
69
- const previewFillPct = computed(() => {
70
- if (max.value === min.value) return 0;
71
- return ((previewValue.value - min.value) / (max.value - min.value)) * 100;
72
- });
73
- const previewFeedback = computed(() => {
74
- return feedback.value.find(
75
- (r) => previewValue.value >= r.min && previewValue.value <= r.max,
76
- );
77
- });
78
- </script>
79
-
80
- <template>
81
- <div class="cpub-slider-edit">
82
- <div class="cpub-slider-edit-header"><i class="fa-solid fa-sliders"></i> Interactive Slider</div>
83
- <div class="cpub-slider-edit-body">
84
- <label class="cpub-edit-label">Label</label>
85
- <input class="cpub-edit-input" :value="label" placeholder="e.g. Learning Rate" @input="update('label', ($event.target as HTMLInputElement).value)" />
86
-
87
- <div class="cpub-edit-row">
88
- <div class="cpub-edit-field">
89
- <label class="cpub-edit-label">Min</label>
90
- <input class="cpub-edit-input" type="number" :value="min" @input="update('min', Number(($event.target as HTMLInputElement).value))" />
91
- </div>
92
- <div class="cpub-edit-field">
93
- <label class="cpub-edit-label">Max</label>
94
- <input class="cpub-edit-input" type="number" :value="max" @input="update('max', Number(($event.target as HTMLInputElement).value))" />
95
- </div>
96
- <div class="cpub-edit-field">
97
- <label class="cpub-edit-label">Step</label>
98
- <input class="cpub-edit-input" type="number" :value="step" @input="update('step', Number(($event.target as HTMLInputElement).value))" />
99
- </div>
100
- <div class="cpub-edit-field">
101
- <label class="cpub-edit-label">Unit</label>
102
- <input class="cpub-edit-input" :value="unit" placeholder="e.g. MHz" @input="update('unit', ($event.target as HTMLInputElement).value)" />
103
- </div>
104
- </div>
105
-
106
- <div class="cpub-edit-field">
107
- <label class="cpub-edit-label">Default Value</label>
108
- <input class="cpub-edit-input" type="number" :value="defaultValue" :min="min" :max="max" @input="update('defaultValue', Number(($event.target as HTMLInputElement).value))" />
109
- </div>
110
-
111
- <!-- Feedback Ranges -->
112
- <div class="cpub-feedback-section">
113
- <div class="cpub-feedback-header">
114
- <span class="cpub-edit-label" style="margin: 0">Feedback Ranges</span>
115
- <span class="cpub-feedback-hint">Define what different slider positions mean</span>
116
- </div>
117
-
118
- <div v-if="feedback.length === 0" class="cpub-feedback-empty">
119
- <i class="fa-solid fa-comment-dots"></i>
120
- <span>No feedback ranges configured. The slider will show a value but no contextual message.</span>
121
- </div>
122
-
123
- <div
124
- v-for="(range, i) in feedback"
125
- :key="i"
126
- class="cpub-feedback-range"
127
- >
128
- <div class="cpub-range-top-row">
129
- <div class="cpub-range-bounds">
130
- <input
131
- class="cpub-range-input"
132
- type="number"
133
- :value="range.min"
134
- title="Range min"
135
- @input="updateFeedbackRange(i, 'min', ($event.target as HTMLInputElement).value)"
136
- />
137
- <span class="cpub-range-dash">—</span>
138
- <input
139
- class="cpub-range-input"
140
- type="number"
141
- :value="range.max"
142
- title="Range max"
143
- @input="updateFeedbackRange(i, 'max', ($event.target as HTMLInputElement).value)"
144
- />
145
- </div>
146
- <select
147
- class="cpub-range-state"
148
- :value="range.state"
149
- @change="updateFeedbackRange(i, 'state', ($event.target as HTMLSelectElement).value)"
150
- >
151
- <option v-for="opt in stateOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
152
- </select>
153
- <button class="cpub-range-remove" title="Remove range" @click="removeFeedbackRange(i)">
154
- <i class="fa-solid fa-xmark"></i>
155
- </button>
156
- </div>
157
- <input
158
- class="cpub-edit-input cpub-range-message"
159
- :value="range.message"
160
- placeholder="What does this range mean? e.g. 'Too slow — model won't converge'"
161
- @input="updateFeedbackRange(i, 'message', ($event.target as HTMLInputElement).value)"
162
- />
163
- </div>
164
-
165
- <button class="cpub-feedback-add" @click="addFeedbackRange">
166
- <i class="fa-solid fa-plus"></i> Add feedback range
167
- </button>
168
- </div>
169
-
170
- <!-- Live Preview -->
171
- <div class="cpub-slider-live-preview">
172
- <div class="cpub-preview-label">Live Preview</div>
173
- <div class="cpub-preview-value">{{ previewValue }}{{ unit }}</div>
174
- <div class="cpub-preview-track-wrap">
175
- <div class="cpub-preview-fill" :style="{ width: previewFillPct + '%' }"></div>
176
- <input
177
- v-model.number="previewValue"
178
- type="range"
179
- class="cpub-preview-range"
180
- :min="min"
181
- :max="max"
182
- :step="step"
183
- />
184
- </div>
185
- <div v-if="previewFeedback" class="cpub-preview-feedback" :class="`state-${previewFeedback.state}`">
186
- {{ previewFeedback.message }}
187
- </div>
188
- <div v-else class="cpub-preview-feedback cpub-preview-feedback--empty">
189
- No feedback for this value range
190
- </div>
191
- </div>
192
- </div>
193
- </div>
194
- </template>
195
-
196
- <style scoped>
197
- .cpub-slider-edit { border: var(--border-width-default) solid var(--accent-border); background: var(--surface); }
198
- .cpub-slider-edit-header { padding: 8px 12px; font-size: 12px; font-weight: 600; background: var(--accent-bg); border-bottom: var(--border-width-default) solid var(--accent-border); display: flex; align-items: center; gap: 8px; color: var(--accent); }
199
- .cpub-slider-edit-body { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
200
- .cpub-edit-label { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 2px; display: block; }
201
- .cpub-edit-input { width: 100%; font-size: 12px; background: var(--surface2); border: var(--border-width-default) solid var(--border2); padding: 6px 8px; color: var(--text); outline: none; }
202
- .cpub-edit-input:focus { border-color: var(--accent); }
203
- .cpub-edit-row { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 8px; }
204
- .cpub-edit-field { display: flex; flex-direction: column; }
205
-
206
- /* Feedback Ranges */
207
- .cpub-feedback-section { margin-top: 4px; border-top: var(--border-width-default) solid var(--border2); padding-top: 10px; }
208
- .cpub-feedback-header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
209
- .cpub-feedback-hint { font-size: 10px; color: var(--text-dim); }
210
-
211
- .cpub-feedback-empty {
212
- padding: 10px 12px;
213
- background: var(--surface2);
214
- border: 2px dashed var(--border2);
215
- font-size: 11px;
216
- color: var(--text-dim);
217
- display: flex;
218
- align-items: center;
219
- gap: 8px;
220
- margin-bottom: 8px;
221
- }
222
- .cpub-feedback-empty i { color: var(--text-faint); }
223
-
224
- .cpub-feedback-range {
225
- background: var(--surface2);
226
- border: var(--border-width-default) solid var(--border2);
227
- padding: 8px;
228
- margin-bottom: 4px;
229
- display: flex;
230
- flex-direction: column;
231
- gap: 6px;
232
- }
233
-
234
- .cpub-range-top-row { display: flex; align-items: center; gap: 6px; }
235
-
236
- .cpub-range-bounds { display: flex; align-items: center; gap: 4px; }
237
- .cpub-range-input {
238
- width: 60px; font-size: 11px; font-family: var(--font-mono);
239
- background: var(--surface); border: var(--border-width-default) solid var(--border2);
240
- padding: 4px 6px; color: var(--text); outline: none; text-align: center;
241
- }
242
- .cpub-range-input:focus { border-color: var(--accent); }
243
- .cpub-range-dash { font-size: 10px; color: var(--text-faint); }
244
-
245
- .cpub-range-state {
246
- font-size: 11px; font-family: var(--font-mono);
247
- background: var(--surface); border: var(--border-width-default) solid var(--border2);
248
- padding: 4px 6px; color: var(--text); outline: none;
249
- min-width: 70px;
250
- }
251
-
252
- .cpub-range-remove {
253
- background: none; border: none; color: var(--text-faint);
254
- cursor: pointer; padding: 2px 4px; font-size: 10px; margin-left: auto;
255
- }
256
- .cpub-range-remove:hover { color: var(--red); }
257
-
258
- .cpub-range-message { font-size: 11px; }
259
-
260
- .cpub-feedback-add {
261
- width: 100%;
262
- font-size: 11px;
263
- color: var(--text-dim);
264
- background: none;
265
- border: 2px dashed var(--border2);
266
- padding: 6px 12px;
267
- cursor: pointer;
268
- display: flex;
269
- align-items: center;
270
- justify-content: center;
271
- gap: 6px;
272
- margin-top: 4px;
273
- }
274
- .cpub-feedback-add:hover { border-color: var(--accent); color: var(--accent); }
275
-
276
- /* Live Preview */
277
- .cpub-slider-live-preview {
278
- margin-top: 4px;
279
- border-top: var(--border-width-default) solid var(--border2);
280
- padding-top: 10px;
281
- }
282
- .cpub-preview-label { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
283
- .cpub-preview-value { font-family: var(--font-mono); font-size: 18px; font-weight: 700; color: var(--accent); margin-bottom: 8px; }
284
-
285
- .cpub-preview-track-wrap { position: relative; margin-bottom: 6px; }
286
- .cpub-preview-fill {
287
- position: absolute; top: 50%; left: 0; height: 4px;
288
- background: var(--accent); transform: translateY(-50%);
289
- pointer-events: none; transition: width 0.05s;
290
- }
291
- .cpub-preview-range {
292
- -webkit-appearance: none; appearance: none;
293
- width: 100%; height: 4px; background: var(--surface3);
294
- border: var(--border-width-default) solid var(--border2); outline: none; cursor: pointer;
295
- position: relative; z-index: 1;
296
- }
297
- .cpub-preview-range::-webkit-slider-thumb {
298
- -webkit-appearance: none; appearance: none;
299
- width: 14px; height: 14px; background: var(--accent);
300
- border: var(--border-width-default) solid var(--border); cursor: pointer;
301
- }
302
- .cpub-preview-range::-moz-range-thumb {
303
- width: 14px; height: 14px; background: var(--accent);
304
- border: var(--border-width-default) solid var(--border); cursor: pointer;
305
- }
306
-
307
- .cpub-preview-feedback {
308
- font-size: 11px; padding: 6px 10px; margin-top: 4px;
309
- display: flex; align-items: center; gap: 6px;
310
- }
311
- .cpub-preview-feedback.state-slow,
312
- .cpub-preview-feedback.state-low { background: var(--yellow-bg); border: var(--border-width-default) solid var(--yellow-border); color: var(--yellow); }
313
- .cpub-preview-feedback.state-ok,
314
- .cpub-preview-feedback.state-good { background: var(--green-bg); border: var(--border-width-default) solid var(--green-border); color: var(--green); }
315
- .cpub-preview-feedback.state-high,
316
- .cpub-preview-feedback.state-danger { background: var(--red-bg); border: var(--border-width-default) solid var(--red-border); color: var(--red); }
317
- .cpub-preview-feedback--empty { background: var(--surface2); border: 2px dashed var(--border2); color: var(--text-faint); font-style: italic; }
318
- </style>