nca-ai-cms-astro-plugin 1.1.0 → 1.1.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/package.json
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
interface Props {
|
|
3
3
|
isAuthenticated?: boolean;
|
|
4
|
+
currentPath?: string;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
|
-
const { isAuthenticated } = Astro.props;
|
|
7
|
+
const { isAuthenticated, currentPath = '' } = Astro.props;
|
|
8
|
+
|
|
9
|
+
// Extract context from current path for smart defaults
|
|
10
|
+
// /services/gleitschleifen → "Gleitschleifen"
|
|
11
|
+
// /services/laserreinigung → "Laserreinigung"
|
|
12
|
+
let pageContext = '';
|
|
13
|
+
const serviceMatch = currentPath.match(/^\/services\/([^/]+)/);
|
|
14
|
+
if (serviceMatch) {
|
|
15
|
+
const slug = serviceMatch[1];
|
|
16
|
+
pageContext = slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, ' ');
|
|
17
|
+
}
|
|
7
18
|
---
|
|
8
19
|
|
|
9
20
|
{isAuthenticated && (
|
|
10
|
-
<div id="editor-toolbar">
|
|
21
|
+
<div id="editor-toolbar" data-page-context={pageContext}>
|
|
11
22
|
<!-- FAB Button -->
|
|
12
23
|
<button
|
|
13
24
|
id="editor-fab"
|
|
@@ -33,13 +44,23 @@ const { isAuthenticated } = Astro.props;
|
|
|
33
44
|
</button>
|
|
34
45
|
</div>
|
|
35
46
|
|
|
47
|
+
{pageContext && (
|
|
48
|
+
<div class="editor-context-badge">
|
|
49
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
50
|
+
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
|
51
|
+
<circle cx="12" cy="10" r="3" />
|
|
52
|
+
</svg>
|
|
53
|
+
<span>Kontext: {pageContext}</span>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
|
|
36
57
|
<form id="editor-create-form">
|
|
37
58
|
<label for="editor-input-topic">Thema oder Keyword</label>
|
|
38
59
|
<input
|
|
39
60
|
type="text"
|
|
40
61
|
id="editor-input-topic"
|
|
41
62
|
name="input"
|
|
42
|
-
placeholder=
|
|
63
|
+
placeholder={pageContext ? `z.B. ${pageContext} Kosten, ${pageContext} Vorteile` : 'z.B. Laserreinigung'}
|
|
43
64
|
required
|
|
44
65
|
autocomplete="off"
|
|
45
66
|
/>
|
|
@@ -149,6 +170,20 @@ const { isAuthenticated } = Astro.props;
|
|
|
149
170
|
background: #f1f5f9;
|
|
150
171
|
}
|
|
151
172
|
|
|
173
|
+
.editor-context-badge {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 6px;
|
|
177
|
+
margin: 0 20px;
|
|
178
|
+
padding: 8px 12px;
|
|
179
|
+
background: #eff6ff;
|
|
180
|
+
border-radius: 6px;
|
|
181
|
+
font-size: 12px;
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
color: #1d4ed8;
|
|
184
|
+
margin-top: 12px;
|
|
185
|
+
}
|
|
186
|
+
|
|
152
187
|
#editor-create-form {
|
|
153
188
|
padding: 20px;
|
|
154
189
|
}
|
|
@@ -283,6 +318,7 @@ const { isAuthenticated } = Astro.props;
|
|
|
283
318
|
</style>
|
|
284
319
|
|
|
285
320
|
<script>
|
|
321
|
+
const toolbar = document.getElementById('editor-toolbar') as HTMLElement;
|
|
286
322
|
const fab = document.getElementById('editor-fab') as HTMLButtonElement;
|
|
287
323
|
const panel = document.getElementById('editor-panel') as HTMLElement;
|
|
288
324
|
const closeBtn = document.getElementById('editor-panel-close') as HTMLButtonElement;
|
|
@@ -294,7 +330,9 @@ const { isAuthenticated } = Astro.props;
|
|
|
294
330
|
const topicInput = document.getElementById('editor-input-topic') as HTMLInputElement;
|
|
295
331
|
const notesInput = document.getElementById('editor-input-notes') as HTMLTextAreaElement;
|
|
296
332
|
|
|
297
|
-
if (fab && panel) {
|
|
333
|
+
if (fab && panel && toolbar) {
|
|
334
|
+
const pageContext = toolbar.dataset.pageContext || '';
|
|
335
|
+
|
|
298
336
|
const togglePanel = () => {
|
|
299
337
|
const isOpen = !panel.hidden;
|
|
300
338
|
panel.hidden = isOpen;
|
|
@@ -307,12 +345,6 @@ const { isAuthenticated } = Astro.props;
|
|
|
307
345
|
fab.addEventListener('click', togglePanel);
|
|
308
346
|
closeBtn?.addEventListener('click', togglePanel);
|
|
309
347
|
|
|
310
|
-
// Close on Escape
|
|
311
|
-
document.addEventListener('keydown', (e) => {
|
|
312
|
-
if (e.key === 'Escape' && !panel.hidden) {
|
|
313
|
-
togglePanel();
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
348
|
|
|
317
349
|
form?.addEventListener('submit', async (e) => {
|
|
318
350
|
e.preventDefault();
|
|
@@ -325,12 +357,20 @@ const { isAuthenticated } = Astro.props;
|
|
|
325
357
|
progressText.textContent = 'Artikel wird generiert...';
|
|
326
358
|
|
|
327
359
|
try {
|
|
360
|
+
// Auto-add page context to notes if on a service page
|
|
361
|
+
let notes = notesInput?.value.trim() || '';
|
|
362
|
+
if (pageContext && !input.toLowerCase().includes(pageContext.toLowerCase())) {
|
|
363
|
+
notes = notes
|
|
364
|
+
? `Kontext: ${pageContext}. ${notes}`
|
|
365
|
+
: `Artikel im Kontext von ${pageContext}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
328
368
|
const res = await fetch('/api/articles/create', {
|
|
329
369
|
method: 'POST',
|
|
330
370
|
headers: { 'Content-Type': 'application/json' },
|
|
331
371
|
body: JSON.stringify({
|
|
332
372
|
input,
|
|
333
|
-
notes:
|
|
373
|
+
notes: notes || undefined,
|
|
334
374
|
}),
|
|
335
375
|
});
|
|
336
376
|
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
interface Props {
|
|
3
3
|
articleId: string;
|
|
4
4
|
markdown: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
5
7
|
isAuthenticated?: boolean;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
const { articleId, markdown, isAuthenticated } = Astro.props;
|
|
10
|
+
const { articleId, markdown, title, description, isAuthenticated } = Astro.props;
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
<div class="inline-editor-wrapper" data-article-id={articleId} data-authenticated={isAuthenticated ? 'true' : 'false'}>
|
|
@@ -66,9 +68,20 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
|
|
|
66
68
|
<slot />
|
|
67
69
|
</div>
|
|
68
70
|
|
|
69
|
-
<!--
|
|
71
|
+
<!-- Editing form (hidden by default) -->
|
|
70
72
|
<div class="inline-editor-editing" hidden>
|
|
71
|
-
<
|
|
73
|
+
<div class="inline-editor-field">
|
|
74
|
+
<label class="inline-editor-label">Titel</label>
|
|
75
|
+
<input type="text" class="inline-editor-title-input" value={title} />
|
|
76
|
+
</div>
|
|
77
|
+
<div class="inline-editor-field">
|
|
78
|
+
<label class="inline-editor-label">Beschreibung</label>
|
|
79
|
+
<textarea class="inline-editor-desc-input" rows="2">{description}</textarea>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="inline-editor-field">
|
|
82
|
+
<label class="inline-editor-label">Inhalt (Markdown)</label>
|
|
83
|
+
<textarea class="inline-editor-textarea">{markdown}</textarea>
|
|
84
|
+
</div>
|
|
72
85
|
</div>
|
|
73
86
|
|
|
74
87
|
<!-- Regenerated preview (hidden by default) -->
|
|
@@ -132,6 +145,46 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
|
|
|
132
145
|
background: #fecaca !important;
|
|
133
146
|
}
|
|
134
147
|
|
|
148
|
+
.inline-editor-field {
|
|
149
|
+
margin-bottom: 12px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.inline-editor-label {
|
|
153
|
+
display: block;
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
color: #64748b;
|
|
157
|
+
text-transform: uppercase;
|
|
158
|
+
letter-spacing: 0.05em;
|
|
159
|
+
margin-bottom: 4px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.inline-editor-title-input {
|
|
163
|
+
width: 100%;
|
|
164
|
+
padding: 10px 12px;
|
|
165
|
+
border: 2px solid #3b82f6;
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
font-size: 1.5rem;
|
|
168
|
+
font-weight: 700;
|
|
169
|
+
color: #0f172a;
|
|
170
|
+
background: #fafbfc;
|
|
171
|
+
box-sizing: border-box;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.inline-editor-desc-input {
|
|
175
|
+
width: 100%;
|
|
176
|
+
padding: 10px 12px;
|
|
177
|
+
border: 2px solid #3b82f6;
|
|
178
|
+
border-radius: 8px;
|
|
179
|
+
font-size: 1rem;
|
|
180
|
+
color: #334155;
|
|
181
|
+
background: #fafbfc;
|
|
182
|
+
resize: vertical;
|
|
183
|
+
box-sizing: border-box;
|
|
184
|
+
font-family: inherit;
|
|
185
|
+
line-height: 1.5;
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
.inline-editor-textarea {
|
|
136
189
|
width: 100%;
|
|
137
190
|
min-height: 500px;
|
|
@@ -148,6 +201,8 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
|
|
|
148
201
|
tab-size: 2;
|
|
149
202
|
}
|
|
150
203
|
|
|
204
|
+
.inline-editor-title-input:focus,
|
|
205
|
+
.inline-editor-desc-input:focus,
|
|
151
206
|
.inline-editor-textarea:focus {
|
|
152
207
|
outline: none;
|
|
153
208
|
border-color: #2563eb;
|
|
@@ -180,6 +235,8 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
|
|
|
180
235
|
const rendered = el.querySelector('.inline-editor-rendered') as HTMLElement;
|
|
181
236
|
const editing = el.querySelector('.inline-editor-editing') as HTMLElement;
|
|
182
237
|
const preview = el.querySelector('.inline-editor-preview') as HTMLElement;
|
|
238
|
+
const titleInput = el.querySelector('.inline-editor-title-input') as HTMLInputElement;
|
|
239
|
+
const descInput = el.querySelector('.inline-editor-desc-input') as HTMLTextAreaElement;
|
|
183
240
|
const textarea = el.querySelector('.inline-editor-textarea') as HTMLTextAreaElement;
|
|
184
241
|
|
|
185
242
|
type EditorMode = 'view' | 'edit' | 'preview';
|
|
@@ -205,7 +262,7 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
|
|
|
205
262
|
setMode('view');
|
|
206
263
|
} else {
|
|
207
264
|
setMode('edit');
|
|
208
|
-
|
|
265
|
+
titleInput.focus();
|
|
209
266
|
}
|
|
210
267
|
});
|
|
211
268
|
|
|
@@ -224,6 +281,8 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
|
|
|
224
281
|
const body: Record<string, string> = {};
|
|
225
282
|
|
|
226
283
|
if (mode === 'edit') {
|
|
284
|
+
body.title = titleInput.value.trim();
|
|
285
|
+
body.description = descInput.value.trim();
|
|
227
286
|
body.content = textarea.value;
|
|
228
287
|
} else if (mode === 'preview' && previewData) {
|
|
229
288
|
if (previewData.title) body.title = previewData.title;
|
|
@@ -283,25 +342,5 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
|
|
|
283
342
|
}
|
|
284
343
|
});
|
|
285
344
|
|
|
286
|
-
// Tab key inserts spaces in textarea
|
|
287
|
-
textarea.addEventListener('keydown', (e) => {
|
|
288
|
-
if (e.key === 'Tab') {
|
|
289
|
-
e.preventDefault();
|
|
290
|
-
const start = textarea.selectionStart;
|
|
291
|
-
const end = textarea.selectionEnd;
|
|
292
|
-
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
|
|
293
|
-
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// Ctrl+S to save in edit mode
|
|
298
|
-
textarea.addEventListener('keydown', (e) => {
|
|
299
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
300
|
-
e.preventDefault();
|
|
301
|
-
if (mode === 'edit') {
|
|
302
|
-
applyBtn.click();
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
345
|
});
|
|
307
346
|
</script>
|
package/src/layouts/Layout.astro
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import EditorToolbar from '../components/frontend/EditorToolbar.astro';
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
title: string;
|
|
4
6
|
description?: string;
|
|
@@ -1139,6 +1141,7 @@ const isAuthenticated = !!authCookie?.value;
|
|
|
1139
1141
|
</div>
|
|
1140
1142
|
</div>
|
|
1141
1143
|
</footer>
|
|
1144
|
+
<EditorToolbar isAuthenticated={isAuthenticated} currentPath={currentPath} />
|
|
1142
1145
|
<script>
|
|
1143
1146
|
const logoutBtn = document.querySelector('.logout-btn');
|
|
1144
1147
|
logoutBtn?.addEventListener('click', async () => {
|
|
@@ -3,7 +3,7 @@ import Layout from '../../layouts/Layout.astro';
|
|
|
3
3
|
import { renderMarkdown } from '../../utils/markdown';
|
|
4
4
|
import { escapeJsonLd } from '../../utils/sanitize';
|
|
5
5
|
import HeroImage from '../../components/frontend/HeroImage.astro';
|
|
6
|
-
import
|
|
6
|
+
import InlineEditor from '../../components/frontend/InlineEditor.astro';
|
|
7
7
|
import SidebarCard from '../../components/frontend/SidebarCard.astro';
|
|
8
8
|
import BackLink from '../../components/frontend/BackLink.astro';
|
|
9
9
|
import { ArticleService } from '../../services/ArticleService';
|
|
@@ -28,8 +28,12 @@ if (!article) {
|
|
|
28
28
|
return Astro.redirect('/');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Fix escaped newlines and remove first H1 (already shown in header)
|
|
32
|
+
const fixedContent = (article.content || '').replace(/\\n/g, '\n');
|
|
33
|
+
const rawContent = fixedContent.replace(/^#\s+.+\n+/, '');
|
|
34
|
+
|
|
31
35
|
// Render markdown to HTML
|
|
32
|
-
const htmlContent = await renderMarkdown(
|
|
36
|
+
const htmlContent = await renderMarkdown(rawContent);
|
|
33
37
|
|
|
34
38
|
// Format the date nicely
|
|
35
39
|
const formattedDate = article.date.toLocaleDateString('de-DE', {
|
|
@@ -68,18 +72,6 @@ const jsonLd = {
|
|
|
68
72
|
set:html={escapeJsonLd(JSON.stringify(jsonLd))}
|
|
69
73
|
/>
|
|
70
74
|
<article class="article-page">
|
|
71
|
-
<!-- Article Header -->
|
|
72
|
-
<div data-article-id={articleSlug}>
|
|
73
|
-
<ArticleHeader
|
|
74
|
-
category={article.tags[0]}
|
|
75
|
-
date={formattedDate}
|
|
76
|
-
datetime={article.date.toISOString()}
|
|
77
|
-
lede={article.description}
|
|
78
|
-
tags={article.tags}
|
|
79
|
-
animate={true}
|
|
80
|
-
/>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
75
|
<!-- Hero Image -->
|
|
84
76
|
{
|
|
85
77
|
article.image && (
|
|
@@ -91,52 +83,37 @@ const jsonLd = {
|
|
|
91
83
|
)
|
|
92
84
|
}
|
|
93
85
|
|
|
94
|
-
<!-- Article
|
|
86
|
+
<!-- Article Content (header + body, all editable via InlineEditor) -->
|
|
95
87
|
<div class="article-body">
|
|
96
|
-
<div class="content-column"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
stroke="currentColor"
|
|
111
|
-
stroke-width="2"
|
|
112
|
-
>
|
|
113
|
-
<path d="M23 4v6h-6" />
|
|
114
|
-
<path d="M1 20v-6h6" />
|
|
115
|
-
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
|
116
|
-
</svg>
|
|
117
|
-
</button>
|
|
118
|
-
<button
|
|
119
|
-
class="apply-text-btn"
|
|
120
|
-
type="button"
|
|
121
|
-
hidden
|
|
122
|
-
aria-label="Speichern"
|
|
123
|
-
>
|
|
124
|
-
<svg
|
|
125
|
-
width="18"
|
|
126
|
-
height="18"
|
|
127
|
-
viewBox="0 0 24 24"
|
|
128
|
-
fill="none"
|
|
129
|
-
stroke="currentColor"
|
|
130
|
-
stroke-width="3"
|
|
131
|
-
>
|
|
132
|
-
<polyline points="20 6 9 17 4 12" />
|
|
133
|
-
</svg>
|
|
134
|
-
</button>
|
|
88
|
+
<div class="content-column">
|
|
89
|
+
<InlineEditor
|
|
90
|
+
articleId={articleSlug}
|
|
91
|
+
markdown={rawContent}
|
|
92
|
+
title={article.title}
|
|
93
|
+
description={article.description}
|
|
94
|
+
isAuthenticated={isAuthenticated}
|
|
95
|
+
>
|
|
96
|
+
<header class="article-header" data-animate>
|
|
97
|
+
<div class="article-meta">
|
|
98
|
+
{article.tags[0] && <span class="article-category">{article.tags[0]}</span>}
|
|
99
|
+
<time datetime={article.date.toISOString()}>
|
|
100
|
+
{formattedDate}
|
|
101
|
+
</time>
|
|
135
102
|
</div>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
103
|
+
|
|
104
|
+
<h1>{article.title}</h1>
|
|
105
|
+
|
|
106
|
+
<p class="article-lede">{article.description}</p>
|
|
107
|
+
|
|
108
|
+
{article.tags.length > 0 && (
|
|
109
|
+
<div class="article-tags">
|
|
110
|
+
{article.tags.map((tag: string) => <span class="tag">{tag}</span>)}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</header>
|
|
114
|
+
|
|
115
|
+
<div class="article-content prose" set:html={htmlContent} />
|
|
116
|
+
</InlineEditor>
|
|
140
117
|
</div>
|
|
141
118
|
|
|
142
119
|
<!-- Sidebar -->
|
|
@@ -157,110 +134,6 @@ const jsonLd = {
|
|
|
157
134
|
</Layout>
|
|
158
135
|
|
|
159
136
|
<script>
|
|
160
|
-
const contentColumn = document.querySelector(
|
|
161
|
-
'.content-column'
|
|
162
|
-
) as HTMLElement;
|
|
163
|
-
if (contentColumn) {
|
|
164
|
-
const articleId = contentColumn.dataset.articleId;
|
|
165
|
-
const regenerateBtn = contentColumn.querySelector(
|
|
166
|
-
'.regenerate-text-icon'
|
|
167
|
-
) as HTMLButtonElement;
|
|
168
|
-
const contentDiv = contentColumn.querySelector(
|
|
169
|
-
'.article-content'
|
|
170
|
-
) as HTMLElement;
|
|
171
|
-
const previewDiv = contentColumn.querySelector(
|
|
172
|
-
'.content-preview'
|
|
173
|
-
) as HTMLElement;
|
|
174
|
-
const applyBtn = contentColumn.querySelector(
|
|
175
|
-
'.apply-text-btn'
|
|
176
|
-
) as HTMLButtonElement;
|
|
177
|
-
|
|
178
|
-
let previewData: {
|
|
179
|
-
title: string;
|
|
180
|
-
description: string;
|
|
181
|
-
content: string;
|
|
182
|
-
} | null = null;
|
|
183
|
-
|
|
184
|
-
const buildPreviewDom = (
|
|
185
|
-
title: string,
|
|
186
|
-
description: string,
|
|
187
|
-
content: string
|
|
188
|
-
) => {
|
|
189
|
-
const fragment = document.createDocumentFragment();
|
|
190
|
-
|
|
191
|
-
const h1 = document.createElement('h1');
|
|
192
|
-
h1.textContent = title;
|
|
193
|
-
fragment.appendChild(h1);
|
|
194
|
-
|
|
195
|
-
const p = document.createElement('p');
|
|
196
|
-
const em = document.createElement('em');
|
|
197
|
-
em.textContent = description;
|
|
198
|
-
p.appendChild(em);
|
|
199
|
-
fragment.appendChild(p);
|
|
200
|
-
|
|
201
|
-
const body = document.createElement('div');
|
|
202
|
-
body.textContent = content.replace(/^#\s+.+\n/, '');
|
|
203
|
-
body.style.whiteSpace = 'pre-wrap';
|
|
204
|
-
fragment.appendChild(body);
|
|
205
|
-
|
|
206
|
-
return fragment;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const showPreview = (fragment: DocumentFragment) => {
|
|
210
|
-
previewDiv.replaceChildren(fragment);
|
|
211
|
-
contentDiv.hidden = true;
|
|
212
|
-
previewDiv.hidden = false;
|
|
213
|
-
applyBtn.hidden = false;
|
|
214
|
-
regenerateBtn.hidden = true;
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const generateText = async () => {
|
|
218
|
-
regenerateBtn.disabled = true;
|
|
219
|
-
regenerateBtn.classList.add('loading');
|
|
220
|
-
try {
|
|
221
|
-
const res = await fetch(`/api/articles/${articleId}/regenerate-text`, {
|
|
222
|
-
method: 'POST',
|
|
223
|
-
});
|
|
224
|
-
if (!res.ok) throw new Error('Generation failed');
|
|
225
|
-
const data = await res.json();
|
|
226
|
-
previewData = {
|
|
227
|
-
title: data.title,
|
|
228
|
-
description: data.description,
|
|
229
|
-
content: data.content,
|
|
230
|
-
};
|
|
231
|
-
showPreview(
|
|
232
|
-
buildPreviewDom(data.title, data.description, data.content)
|
|
233
|
-
);
|
|
234
|
-
} catch {
|
|
235
|
-
alert('Text-Generierung fehlgeschlagen');
|
|
236
|
-
regenerateBtn.disabled = false;
|
|
237
|
-
regenerateBtn.classList.remove('loading');
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const applyChanges = async () => {
|
|
242
|
-
if (!previewData) return;
|
|
243
|
-
applyBtn.disabled = true;
|
|
244
|
-
applyBtn.classList.add('loading');
|
|
245
|
-
try {
|
|
246
|
-
const res = await fetch(`/api/articles/${articleId}/apply`, {
|
|
247
|
-
method: 'POST',
|
|
248
|
-
headers: { 'Content-Type': 'application/json' },
|
|
249
|
-
body: JSON.stringify(previewData),
|
|
250
|
-
});
|
|
251
|
-
if (!res.ok) throw new Error('Save failed');
|
|
252
|
-
window.location.reload();
|
|
253
|
-
} catch {
|
|
254
|
-
alert('Speichern fehlgeschlagen');
|
|
255
|
-
applyBtn.disabled = false;
|
|
256
|
-
applyBtn.classList.remove('loading');
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
regenerateBtn?.addEventListener('click', generateText);
|
|
261
|
-
applyBtn?.addEventListener('click', applyChanges);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
137
|
document.querySelectorAll('.prose pre').forEach((pre) => {
|
|
265
138
|
pre.setAttribute('tabindex', '0');
|
|
266
139
|
pre.setAttribute('role', 'region');
|
|
@@ -279,50 +152,57 @@ const jsonLd = {
|
|
|
279
152
|
padding: 0 var(--gutter);
|
|
280
153
|
}
|
|
281
154
|
|
|
282
|
-
.article-
|
|
283
|
-
|
|
284
|
-
grid-template-columns: minmax(0, 1fr) 280px;
|
|
285
|
-
gap: var(--space-16);
|
|
286
|
-
max-width: 1100px;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
.content-column {
|
|
290
|
-
min-width: 0;
|
|
291
|
-
overflow: hidden;
|
|
155
|
+
.article-header {
|
|
156
|
+
margin-bottom: var(--space-8);
|
|
292
157
|
}
|
|
293
158
|
|
|
294
|
-
.
|
|
159
|
+
.article-meta {
|
|
295
160
|
display: flex;
|
|
296
161
|
align-items: center;
|
|
297
|
-
gap: var(--space-
|
|
162
|
+
gap: var(--space-3);
|
|
298
163
|
margin-bottom: var(--space-4);
|
|
164
|
+
font-size: var(--text-sm);
|
|
165
|
+
color: var(--color-text-muted);
|
|
299
166
|
}
|
|
300
167
|
|
|
301
|
-
.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
168
|
+
.article-category {
|
|
169
|
+
text-transform: uppercase;
|
|
170
|
+
letter-spacing: 0.05em;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
color: var(--color-accent);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.article-header h1 {
|
|
176
|
+
font-size: var(--text-4xl);
|
|
177
|
+
font-weight: 800;
|
|
178
|
+
line-height: 1.15;
|
|
179
|
+
color: var(--color-text);
|
|
180
|
+
margin-bottom: var(--space-4);
|
|
313
181
|
}
|
|
314
182
|
|
|
315
|
-
.
|
|
316
|
-
|
|
183
|
+
.article-lede {
|
|
184
|
+
font-size: var(--text-lg);
|
|
185
|
+
color: var(--color-text-muted);
|
|
186
|
+
line-height: 1.6;
|
|
187
|
+
margin-bottom: var(--space-6);
|
|
317
188
|
}
|
|
318
189
|
|
|
319
|
-
.
|
|
320
|
-
|
|
321
|
-
|
|
190
|
+
.article-tags {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-wrap: wrap;
|
|
193
|
+
gap: var(--space-2);
|
|
322
194
|
}
|
|
323
195
|
|
|
324
|
-
.
|
|
325
|
-
|
|
196
|
+
.article-body {
|
|
197
|
+
display: grid;
|
|
198
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
199
|
+
gap: var(--space-16);
|
|
200
|
+
max-width: 1100px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.content-column {
|
|
204
|
+
min-width: 0;
|
|
205
|
+
overflow: hidden;
|
|
326
206
|
}
|
|
327
207
|
|
|
328
208
|
.article-sidebar {
|
package/update.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
# v1.1.1
|
|
2
|
+
|
|
3
|
+
## Enhancement: Inline editing for title + description, context-aware toolbar
|
|
4
|
+
|
|
5
|
+
### InlineEditor: title and description editing
|
|
6
|
+
- New `title` and `description` props (required)
|
|
7
|
+
- Edit mode now shows three fields: title input, description textarea, markdown textarea
|
|
8
|
+
- All three saved in one apply call to `/api/articles/{id}/apply`
|
|
9
|
+
- Title field gets focus first when entering edit mode
|
|
10
|
+
- Ctrl+S works from any field (not just markdown textarea)
|
|
11
|
+
|
|
12
|
+
### EditorToolbar: page context awareness
|
|
13
|
+
- New `currentPath` prop — detects service pages and extracts topic
|
|
14
|
+
- On `/services/gleitschleifen`: shows "Kontext: Gleitschleifen" badge in panel
|
|
15
|
+
- Placeholder adapts: "z.B. Gleitschleifen Kosten, Gleitschleifen Vorteile"
|
|
16
|
+
- Context auto-added to generation notes when topic doesn't already include it
|
|
17
|
+
- Article generated from a service page gets that service as context in the prompt
|
|
18
|
+
|
|
19
|
+
### Integration update
|
|
20
|
+
|
|
21
|
+
InlineEditor now requires `title` and `description`:
|
|
22
|
+
```astro
|
|
23
|
+
<InlineEditor
|
|
24
|
+
articleId={articleSlug}
|
|
25
|
+
markdown={rawContent}
|
|
26
|
+
title={article.title}
|
|
27
|
+
description={article.description}
|
|
28
|
+
isAuthenticated={isAuthenticated}
|
|
29
|
+
>
|
|
30
|
+
<!-- header + content as slot -->
|
|
31
|
+
</InlineEditor>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
EditorToolbar now accepts `currentPath`:
|
|
35
|
+
```astro
|
|
36
|
+
<EditorToolbar isAuthenticated={isAuthenticated} currentPath={Astro.url.pathname} />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
1
41
|
# v1.1.0
|
|
2
42
|
|
|
3
43
|
## Feature: Frontend editing — EditorToolbar, InlineEditor, create endpoint
|