@millstone/synapse-site 0.1.0

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.
@@ -0,0 +1,402 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="robots" content="noindex" />
7
+ <title>Edit Documentation | Synapse</title>
8
+ <style>
9
+ /* Loading state styling */
10
+ .loading-container {
11
+ display: flex;
12
+ flex-direction: column;
13
+ align-items: center;
14
+ justify-content: center;
15
+ height: 100vh;
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
17
+ background: #f5f5f5;
18
+ }
19
+ .loading-spinner {
20
+ width: 40px;
21
+ height: 40px;
22
+ border: 3px solid #e0e0e0;
23
+ border-top-color: #284b63;
24
+ border-radius: 50%;
25
+ animation: spin 1s linear infinite;
26
+ }
27
+ .loading-text {
28
+ margin-top: 16px;
29
+ color: #666;
30
+ font-size: 14px;
31
+ }
32
+ @keyframes spin {
33
+ to { transform: rotate(360deg); }
34
+ }
35
+
36
+ /* Validation error styling - positioned at bottom */
37
+ .validation-error {
38
+ position: fixed;
39
+ bottom: 0;
40
+ left: 0;
41
+ right: 0;
42
+ background: #d32f2f;
43
+ color: white;
44
+ padding: 16px 24px;
45
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
46
+ z-index: 10000;
47
+ box-shadow: 0 -2px 8px rgba(0,0,0,0.2);
48
+ }
49
+ .validation-error h3 {
50
+ margin: 0 0 8px 0;
51
+ font-size: 16px;
52
+ font-weight: 600;
53
+ }
54
+ .validation-error ul {
55
+ margin: 0;
56
+ padding-left: 20px;
57
+ font-size: 14px;
58
+ }
59
+ .validation-error li {
60
+ margin: 4px 0;
61
+ }
62
+ .validation-error button {
63
+ position: absolute;
64
+ top: 12px;
65
+ right: 12px;
66
+ background: transparent;
67
+ border: none;
68
+ color: white;
69
+ font-size: 20px;
70
+ cursor: pointer;
71
+ padding: 4px 8px;
72
+ }
73
+
74
+ /* Save success notification - positioned at bottom */
75
+ .save-success {
76
+ position: fixed;
77
+ bottom: 0;
78
+ left: 0;
79
+ right: 0;
80
+ background: #2e7d32;
81
+ color: white;
82
+ padding: 16px 24px;
83
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
84
+ z-index: 10000;
85
+ box-shadow: 0 -2px 8px rgba(0,0,0,0.2);
86
+ }
87
+ .save-success h3 {
88
+ margin: 0 0 8px 0;
89
+ font-size: 16px;
90
+ font-weight: 600;
91
+ }
92
+ .save-success p {
93
+ margin: 0;
94
+ font-size: 14px;
95
+ opacity: 0.9;
96
+ }
97
+ .save-success button {
98
+ position: absolute;
99
+ top: 12px;
100
+ right: 12px;
101
+ background: transparent;
102
+ border: none;
103
+ color: white;
104
+ font-size: 20px;
105
+ cursor: pointer;
106
+ padding: 4px 8px;
107
+ }
108
+
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <!-- Loading state shown while CMS initializes -->
113
+ <div id="loading" class="loading-container">
114
+ <div class="loading-spinner"></div>
115
+ <div class="loading-text">Loading editor...</div>
116
+ </div>
117
+
118
+ <!-- Decap CMS will mount here -->
119
+ <script src="https://unpkg.com/decap-cms@^3.1.0/dist/decap-cms.js"></script>
120
+
121
+ <!-- Validation bundle (built at build time) -->
122
+ <script type="module">
123
+ // Import validation context
124
+ const vaultIndex = await fetch('/static/edit/vault-index.json').then(r => r.json());
125
+ const schemas = await fetch('/static/edit/schemas/index.json').then(r => r.json());
126
+ const bodyGrammars = await fetch('/static/edit/body-grammars/index.json').then(r => r.json());
127
+
128
+ // Store validation context globally
129
+ window.validationContext = {
130
+ vaultIndex: vaultIndex.targets,
131
+ schemas,
132
+ bodyGrammars
133
+ };
134
+
135
+ // Hide loading once CMS loads
136
+ document.getElementById('loading').style.display = 'none';
137
+ </script>
138
+
139
+ <script>
140
+ // Wait for CMS to be available
141
+ function initCMS() {
142
+ if (typeof CMS === 'undefined') {
143
+ setTimeout(initCMS, 100);
144
+ return;
145
+ }
146
+
147
+ // Register custom preview styles to match Quartz theme
148
+ CMS.registerPreviewStyle(`
149
+ /* Base styles matching Quartz theme */
150
+ html, body {
151
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
152
+ line-height: 1.6;
153
+ color: #4e4e4e;
154
+ background: #faf8f8;
155
+ padding: 2rem;
156
+ max-width: 800px;
157
+ margin: 0 auto;
158
+ }
159
+
160
+ /* Headings */
161
+ h1, h2, h3, h4, h5, h6 {
162
+ color: #2b2b2b;
163
+ font-weight: 600;
164
+ margin-top: 1.5em;
165
+ margin-bottom: 0.5em;
166
+ line-height: 1.3;
167
+ }
168
+ h1 { font-size: 2rem; border-bottom: 1px solid #e5e5e5; padding-bottom: 0.3em; }
169
+ h2 { font-size: 1.5rem; border-bottom: 1px solid #e5e5e5; padding-bottom: 0.2em; }
170
+ h3 { font-size: 1.25rem; }
171
+ h4 { font-size: 1.1rem; }
172
+
173
+ /* Paragraphs and text */
174
+ p { margin: 1em 0; }
175
+ strong { font-weight: 600; }
176
+ a { color: #284b63; text-decoration: none; font-weight: 600; }
177
+ a:hover { color: #84a59d; }
178
+
179
+ /* Lists */
180
+ ul, ol { padding-left: 1.5em; margin: 1em 0; }
181
+ li { margin: 0.25em 0; }
182
+ li > ul, li > ol { margin: 0.25em 0; }
183
+
184
+ /* Code */
185
+ code {
186
+ background: #e5e5e5;
187
+ padding: 0.2em 0.4em;
188
+ border-radius: 4px;
189
+ font-size: 0.9em;
190
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
191
+ }
192
+ pre {
193
+ background: #2b2b2b;
194
+ color: #faf8f8;
195
+ padding: 1em;
196
+ border-radius: 8px;
197
+ overflow-x: auto;
198
+ margin: 1em 0;
199
+ }
200
+ pre code {
201
+ background: transparent;
202
+ padding: 0;
203
+ color: inherit;
204
+ }
205
+
206
+ /* Blockquotes */
207
+ blockquote {
208
+ border-left: 4px solid #284b63;
209
+ margin: 1em 0;
210
+ padding: 0.5em 1em;
211
+ background: rgba(143, 159, 169, 0.15);
212
+ border-radius: 0 8px 8px 0;
213
+ }
214
+ blockquote p { margin: 0.5em 0; }
215
+
216
+ /* Tables */
217
+ table {
218
+ width: 100%;
219
+ border-collapse: collapse;
220
+ margin: 1em 0;
221
+ }
222
+ th, td {
223
+ border: 1px solid #e5e5e5;
224
+ padding: 0.5em 0.75em;
225
+ text-align: left;
226
+ }
227
+ th {
228
+ background: #e5e5e5;
229
+ font-weight: 600;
230
+ color: #2b2b2b;
231
+ }
232
+ tr:nth-child(even) { background: rgba(143, 159, 169, 0.08); }
233
+
234
+ /* Horizontal rules */
235
+ hr {
236
+ border: none;
237
+ border-top: 2px solid #e5e5e5;
238
+ margin: 2em 0;
239
+ }
240
+
241
+ /* Images */
242
+ img {
243
+ max-width: 100%;
244
+ height: auto;
245
+ border-radius: 8px;
246
+ }
247
+ `, { raw: true });
248
+
249
+ // Register preSave validation hook
250
+ CMS.registerEventListener({
251
+ name: 'preSave',
252
+ handler: async function({ entry }) {
253
+ const context = window.validationContext;
254
+ if (!context) {
255
+ console.warn('Validation context not loaded, skipping validation');
256
+ return entry.get('data');
257
+ }
258
+
259
+ // Extract entry data
260
+ const data = entry.get('data').toJS();
261
+ const body = data.body || '';
262
+ const path = entry.get('path');
263
+
264
+ // Build frontmatter (everything except body)
265
+ const frontmatter = { ...data };
266
+ delete frontmatter.body;
267
+
268
+ // Import validation (dynamically since it's a module)
269
+ try {
270
+ const { validateDocument } = await import('/static/edit/validate.bundle.js');
271
+
272
+ const result = validateDocument(frontmatter, body, path, context);
273
+
274
+ if (!result.success) {
275
+ const errors = result.issues
276
+ .filter(i => i.type === 'error')
277
+ .map(i => i.message);
278
+
279
+ // Show error UI
280
+ showValidationErrors(errors);
281
+
282
+ // Block save by throwing
283
+ throw new Error('Validation failed. Please fix the errors above.');
284
+ }
285
+
286
+ // Clear any previous errors
287
+ clearValidationErrors();
288
+
289
+ } catch (error) {
290
+ if (error.message.includes('Validation failed')) {
291
+ throw error;
292
+ }
293
+ // If validation module fails to load, log but allow save
294
+ console.error('Validation error:', error);
295
+ }
296
+
297
+ return entry.get('data');
298
+ }
299
+ });
300
+
301
+ // Register postSave to show success message
302
+ CMS.registerEventListener({
303
+ name: 'postSave',
304
+ handler: function({ entry }) {
305
+ clearValidationErrors();
306
+ showSaveSuccess(entry.get('path'));
307
+ console.log('Document saved successfully:', entry.get('path'));
308
+ }
309
+ });
310
+
311
+ console.log('Synapse CMS validation initialized');
312
+
313
+ // Rename "Publish" buttons to "Save" for clarity
314
+ // (In our workflow, publish = save to git, which triggers deploy)
315
+ renamePublishButtons();
316
+ }
317
+
318
+ function renamePublishButtons() {
319
+ // Use MutationObserver to catch dynamically rendered buttons
320
+ const observer = new MutationObserver(() => {
321
+ // Find all buttons/elements containing "Publish" text
322
+ document.querySelectorAll('button, [role="button"], span').forEach(el => {
323
+ if (el.textContent === 'Publish') {
324
+ el.textContent = 'Save';
325
+ }
326
+ if (el.textContent === 'Publish now') {
327
+ el.textContent = 'Save now';
328
+ }
329
+ if (el.textContent === 'Publish and create new') {
330
+ el.textContent = 'Save and create new';
331
+ }
332
+ if (el.textContent === 'Publish and duplicate') {
333
+ el.textContent = 'Save and duplicate';
334
+ }
335
+ });
336
+ });
337
+
338
+ observer.observe(document.body, {
339
+ childList: true,
340
+ subtree: true,
341
+ characterData: true
342
+ });
343
+ }
344
+
345
+ function showValidationErrors(errors) {
346
+ clearValidationErrors();
347
+
348
+ const container = document.createElement('div');
349
+ container.className = 'validation-error';
350
+ container.id = 'validation-errors';
351
+ container.innerHTML = `
352
+ <h3>⚠️ Validation Errors</h3>
353
+ <ul>
354
+ ${errors.map(e => `<li>${escapeHtml(e)}</li>`).join('')}
355
+ </ul>
356
+ <button onclick="clearValidationErrors()" aria-label="Dismiss">×</button>
357
+ `;
358
+ document.body.appendChild(container);
359
+ }
360
+
361
+ function clearValidationErrors() {
362
+ const existing = document.getElementById('validation-errors');
363
+ if (existing) {
364
+ existing.remove();
365
+ }
366
+ }
367
+
368
+ function showSaveSuccess(path) {
369
+ clearSaveSuccess();
370
+
371
+ const container = document.createElement('div');
372
+ container.className = 'save-success';
373
+ container.id = 'save-success';
374
+ container.innerHTML = `
375
+ <h3>✓ Changes saved!</h3>
376
+ <p>The documentation site will update in approximately 2-3 minutes.</p>
377
+ <button onclick="clearSaveSuccess()" aria-label="Dismiss">×</button>
378
+ `;
379
+ document.body.appendChild(container);
380
+
381
+ // Auto-dismiss after 10 seconds
382
+ setTimeout(clearSaveSuccess, 10000);
383
+ }
384
+
385
+ function clearSaveSuccess() {
386
+ const existing = document.getElementById('save-success');
387
+ if (existing) {
388
+ existing.remove();
389
+ }
390
+ }
391
+
392
+ function escapeHtml(text) {
393
+ const div = document.createElement('div');
394
+ div.textContent = text;
395
+ return div.innerHTML;
396
+ }
397
+
398
+ // Start CMS initialization
399
+ initCMS();
400
+ </script>
401
+ </body>
402
+ </html>