@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,387 @@
1
+ /**
2
+ * CustomHeaderFooter Plugin for Quartz
3
+ *
4
+ * Adds custom header/footer elements and "Edit in CMS" links to documents.
5
+ * This plugin runs during build time only, so edit links never appear in source files.
6
+ */
7
+
8
+ // Map document types to their CMS collection names
9
+ const typeToCollection: Record<string, string> = {
10
+ policy: "policies",
11
+ standard: "standards",
12
+ process: "processes",
13
+ sop: "sops",
14
+ runbook: "runbooks",
15
+ system: "systems",
16
+ adr: "adrs",
17
+ prd: "prds",
18
+ capability: "capabilities",
19
+ tdd: "tdds",
20
+ }
21
+
22
+ export interface CustomHeaderFooterOptions {
23
+ baseUrl?: string
24
+ position?: "top" | "bottom" | "both"
25
+ showIcon?: boolean
26
+ debug?: boolean
27
+ showAuthoringGuide?: boolean
28
+ showHomeLink?: boolean
29
+ showEditInCMS?: boolean
30
+ }
31
+
32
+ const defaultOptions: CustomHeaderFooterOptions = {
33
+ baseUrl: "/edit",
34
+ position: "both",
35
+ showIcon: true,
36
+ showAuthoringGuide: true,
37
+ showHomeLink: true,
38
+ showEditInCMS: true,
39
+ }
40
+
41
+ /**
42
+ * Quartz transformer plugin that adds custom header/footer elements and "Edit in CMS" links
43
+ */
44
+ export const CustomHeaderFooter = (userOpts?: CustomHeaderFooterOptions) => {
45
+ const opts = { ...defaultOptions, ...userOpts }
46
+
47
+ return {
48
+ name: "CustomHeaderFooter",
49
+ htmlPlugins() {
50
+ return [
51
+ () => {
52
+ return (tree: any, file: any) => {
53
+ try {
54
+ // Error recovery: ensure file and data exist
55
+ if (!file || !file.data) {
56
+ return
57
+ }
58
+
59
+ const filePath = file.path || file.history?.[0] || "unknown"
60
+
61
+ // Add custom header with navigation links
62
+ const headerNav = {
63
+ type: "element",
64
+ tagName: "nav",
65
+ properties: {
66
+ className: ["custom-header-nav"],
67
+ style: "padding: 1rem 0; margin-bottom: 2rem; border-bottom: 2px solid var(--lightgray); display: flex; gap: 1.5rem; align-items: center;"
68
+ },
69
+ children: [
70
+ ...(opts.showHomeLink ? [{
71
+ type: "element",
72
+ tagName: "a",
73
+ properties: {
74
+ href: "/",
75
+ className: ["nav-link"],
76
+ style: "color: var(--dark); text-decoration: none; font-weight: 600; font-size: 1.1rem;"
77
+ },
78
+ children: [{
79
+ type: "text",
80
+ value: "🧬 Synapse Docs"
81
+ }]
82
+ }] : []),
83
+ ...(opts.showAuthoringGuide ? [{
84
+ type: "element",
85
+ tagName: "a",
86
+ properties: {
87
+ href: "/00_Guides/Authoring-Guide",
88
+ className: ["nav-link"],
89
+ style: "color: var(--dark); text-decoration: none; font-weight: 500;"
90
+ },
91
+ children: [{
92
+ type: "text",
93
+ value: "📚 Authoring Guide"
94
+ }]
95
+ }] : []),
96
+ {
97
+ type: "element",
98
+ tagName: "a",
99
+ properties: {
100
+ href: "/00_Guides/Examples",
101
+ className: ["nav-link"],
102
+ style: "color: var(--dark); text-decoration: none; font-weight: 500;"
103
+ },
104
+ children: [{
105
+ type: "text",
106
+ value: "📝 Examples"
107
+ }]
108
+ }
109
+ ]
110
+ }
111
+
112
+ // Add custom footer with useful links
113
+ const footerNav = {
114
+ type: "element",
115
+ tagName: "footer",
116
+ properties: {
117
+ className: ["custom-footer-nav"],
118
+ style: "padding: 2rem 0; margin-top: 4rem; border-top: 2px solid var(--lightgray); display: flex; flex-wrap: wrap; gap: 2rem; justify-content: space-between; align-items: center;"
119
+ },
120
+ children: [
121
+ {
122
+ type: "element",
123
+ tagName: "div",
124
+ properties: {
125
+ style: "display: flex; gap: 1.5rem; flex-wrap: wrap;"
126
+ },
127
+ children: [
128
+ ...(opts.showAuthoringGuide ? [{
129
+ type: "element",
130
+ tagName: "a",
131
+ properties: {
132
+ href: "/00_Guides/Authoring-Guide",
133
+ className: ["footer-link"],
134
+ style: "color: var(--darkgray); text-decoration: none;"
135
+ },
136
+ children: [{
137
+ type: "text",
138
+ value: "📚 Authoring Guide"
139
+ }]
140
+ }] : []),
141
+ {
142
+ type: "element",
143
+ tagName: "a",
144
+ properties: {
145
+ href: "/00_Guides/Examples",
146
+ className: ["footer-link"],
147
+ style: "color: var(--darkgray); text-decoration: none;"
148
+ },
149
+ children: [{
150
+ type: "text",
151
+ value: "Examples"
152
+ }]
153
+ },
154
+ {
155
+ type: "element",
156
+ tagName: "a",
157
+ properties: {
158
+ href: "/edit",
159
+ className: ["footer-link"],
160
+ style: "color: var(--darkgray); text-decoration: none;"
161
+ },
162
+ children: [{
163
+ type: "text",
164
+ value: "Edit in CMS"
165
+ }]
166
+ }
167
+ ]
168
+ },
169
+ {
170
+ type: "element",
171
+ tagName: "div",
172
+ properties: {
173
+ style: "color: var(--gray); font-size: 0.9rem;"
174
+ },
175
+ children: [{
176
+ type: "text",
177
+ value: "Powered by Synapse Documentation Framework"
178
+ }]
179
+ }
180
+ ]
181
+ }
182
+
183
+ // Debug logging for TDD files
184
+ if (filePath.includes("tdd") || filePath.includes("TDD")) {
185
+ console.log("Processing TDD file:", filePath)
186
+ console.log("Frontmatter:", file.data?.frontmatter)
187
+ }
188
+
189
+ // Check if document has a type in frontmatter for Edit in CMS functionality
190
+ let editLinkElement = null
191
+ let editLinkElementTop = null
192
+
193
+ if (opts.showEditInCMS && file.data?.frontmatter) {
194
+ const docType = file.data.frontmatter.type
195
+ if (docType && typeToCollection[docType]) {
196
+ const collection = typeToCollection[docType]
197
+ let slug = file.data?.slug || ""
198
+
199
+ if (slug) {
200
+ // Extract the path relative to collection folder
201
+ // e.g., "30_Processes/examples/example-change-management-process"
202
+ // -> "examples/example-change-management-process"
203
+ const pathParts = slug.split('/')
204
+ // Skip the first part (e.g., "30_Processes") to get relative path
205
+ const relativePath = pathParts.length > 1
206
+ ? pathParts.slice(1).join('/')
207
+ : pathParts[0]
208
+ const editUrl = `/static/edit/#/collections/${collection}/entries/${relativePath}`
209
+
210
+ // Create the edit link element
211
+ editLinkElement = {
212
+ type: "element",
213
+ tagName: "div",
214
+ properties: {
215
+ className: ["edit-in-cms"],
216
+ style: "margin: 2rem 0; padding: 1rem 0; border-top: 1px solid var(--lightgray);"
217
+ },
218
+ children: [
219
+ {
220
+ type: "element",
221
+ tagName: "a",
222
+ properties: {
223
+ href: "#",
224
+ "data-cms-url": editUrl,
225
+ target: "_blank",
226
+ rel: "noopener noreferrer",
227
+ className: ["edit-in-cms-link"],
228
+ onclick: "event.preventDefault(); window.open(this.dataset.cmsUrl, '_blank');",
229
+ style: "display: inline-flex; align-items: center; padding: 0.5rem 1rem; background-color: var(--lightgray); color: var(--dark); text-decoration: none; border-radius: 6px; font-weight: 500; transition: all 0.2s ease; cursor: pointer;"
230
+ },
231
+ children: [
232
+ ...(opts.showIcon ? [{
233
+ type: "element",
234
+ tagName: "svg",
235
+ properties: {
236
+ xmlns: "http://www.w3.org/2000/svg",
237
+ width: "14",
238
+ height: "14",
239
+ viewBox: "0 0 24 24",
240
+ fill: "none",
241
+ stroke: "currentColor",
242
+ "stroke-width": "2",
243
+ "stroke-linecap": "round",
244
+ "stroke-linejoin": "round",
245
+ style: "display: inline-block; vertical-align: middle; margin-right: 0.3rem;"
246
+ },
247
+ children: [
248
+ {
249
+ type: "element",
250
+ tagName: "path",
251
+ properties: {
252
+ d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
253
+ },
254
+ children: []
255
+ },
256
+ {
257
+ type: "element",
258
+ tagName: "path",
259
+ properties: {
260
+ d: "M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
261
+ },
262
+ children: []
263
+ }
264
+ ]
265
+ }] : []),
266
+ {
267
+ type: "element",
268
+ tagName: "span",
269
+ properties: {},
270
+ children: [
271
+ {
272
+ type: "text",
273
+ value: "Edit in CMS"
274
+ }
275
+ ]
276
+ }
277
+ ]
278
+ }
279
+ ]
280
+ }
281
+
282
+ // Create a second edit link element for top position if needed
283
+ if (opts.position === "both") {
284
+ editLinkElementTop = JSON.parse(JSON.stringify(editLinkElement))
285
+ editLinkElementTop.properties.className = ["edit-in-cms", "edit-in-cms-top"]
286
+ editLinkElementTop.properties.style = "margin: 0 0 2rem 0; padding: 1rem 0; border-bottom: 1px solid var(--lightgray);"
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ // Add CSS styles
293
+ const styleElement = {
294
+ type: "element",
295
+ tagName: "style",
296
+ properties: {},
297
+ children: [
298
+ {
299
+ type: "text",
300
+ value: `
301
+ .custom-header-nav .nav-link:hover {
302
+ color: var(--secondary) !important;
303
+ text-decoration: underline !important;
304
+ }
305
+ .custom-footer-nav .footer-link:hover {
306
+ color: var(--dark) !important;
307
+ text-decoration: underline !important;
308
+ }
309
+ .edit-in-cms-link:hover {
310
+ background-color: var(--gray) !important;
311
+ transform: translateY(-1px);
312
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
313
+ }
314
+ :root[saved-theme="dark"] .edit-in-cms-link {
315
+ background-color: var(--lightgray);
316
+ color: var(--light);
317
+ }
318
+ :root[saved-theme="dark"] .custom-header-nav .nav-link {
319
+ color: var(--light);
320
+ }
321
+ :root[saved-theme="dark"] .custom-footer-nav .footer-link {
322
+ color: var(--lightgray);
323
+ }
324
+ .edit-in-cms-top {
325
+ margin-top: 0 !important;
326
+ }
327
+ `
328
+ }
329
+ ]
330
+ }
331
+
332
+ // Add JavaScript to handle clicks
333
+ const scriptElement = {
334
+ type: "element",
335
+ tagName: "script",
336
+ properties: {},
337
+ children: [
338
+ {
339
+ type: "text",
340
+ value: `
341
+ document.addEventListener('DOMContentLoaded', function() {
342
+ document.querySelectorAll('.edit-in-cms-link').forEach(function(link) {
343
+ link.addEventListener('click', function(e) {
344
+ e.preventDefault();
345
+ window.open(this.dataset.cmsUrl, '_blank');
346
+ });
347
+ });
348
+ });
349
+ `
350
+ }
351
+ ]
352
+ }
353
+
354
+ // Build the tree with all elements
355
+ tree.children = tree.children || []
356
+
357
+ // Add header navigation at the very top
358
+ tree.children.unshift(headerNav)
359
+
360
+ // Add edit links based on position setting
361
+ if (editLinkElement) {
362
+ if (opts.position === "bottom") {
363
+ tree.children.push(editLinkElement)
364
+ } else if (opts.position === "top") {
365
+ tree.children.splice(2, 0, editLinkElement) // After header nav
366
+ } else if (opts.position === "both" && editLinkElementTop) {
367
+ tree.children.splice(2, 0, editLinkElementTop) // After header nav
368
+ tree.children.push(editLinkElement) // At bottom
369
+ }
370
+ }
371
+
372
+ // Add footer and scripts at the end
373
+ tree.children.push(footerNav)
374
+ tree.children.push(styleElement)
375
+ tree.children.push(scriptElement)
376
+
377
+ } catch (error) {
378
+ if (opts.debug) {
379
+ console.error('CustomHeaderFooter plugin error:', error)
380
+ }
381
+ }
382
+ }
383
+ }
384
+ ]
385
+ }
386
+ }
387
+ }
@@ -0,0 +1,107 @@
1
+ import { QuartzConfig } from "./quartz/cfg"
2
+ import * as Plugin from "./quartz/plugins"
3
+ import { CustomHeaderFooter } from "./plugins/CustomHeaderFooter"
4
+
5
+ /**
6
+ * Quartz 4 Configuration for Synapse Documentation Framework
7
+ *
8
+ * This file should be copied into the Quartz submodule root after initialization.
9
+ * The configuration enables graph visualization, backlinks, and tags as required
10
+ * by SYN-P1-T02.
11
+ */
12
+ const config: QuartzConfig = {
13
+ configuration: {
14
+ pageTitle: "🧬 Synapse Documentation",
15
+ enableSPA: true,
16
+ enablePopovers: true,
17
+ analytics: null,
18
+ locale: "en-US",
19
+ baseUrl: "synapse.local",
20
+ ignorePatterns: ["private", "templates", ".obsidian", "_assets", "examples/*.yaml"],
21
+ defaultDateType: "created",
22
+ theme: {
23
+ fontOrigin: "googleFonts",
24
+ cdnCaching: true,
25
+ typography: {
26
+ header: "Schibsted Grotesk",
27
+ body: "Source Sans Pro",
28
+ code: "IBM Plex Mono",
29
+ },
30
+ colors: {
31
+ lightMode: {
32
+ light: "#faf8f8",
33
+ lightgray: "#e5e5e5",
34
+ gray: "#b8b8b8",
35
+ darkgray: "#4e4e4e",
36
+ dark: "#2b2b2b",
37
+ secondary: "#284b63",
38
+ tertiary: "#84a59d",
39
+ highlight: "rgba(143, 159, 169, 0.15)",
40
+ textHighlight: "#fff23688",
41
+ },
42
+ darkMode: {
43
+ light: "#161618",
44
+ lightgray: "#393639",
45
+ gray: "#646464",
46
+ darkgray: "#d4d4d4",
47
+ dark: "#ebebec",
48
+ secondary: "#7b97aa",
49
+ tertiary: "#84a59d",
50
+ highlight: "rgba(143, 159, 169, 0.15)",
51
+ textHighlight: "#b3aa0288",
52
+ },
53
+ },
54
+ },
55
+ },
56
+ plugins: {
57
+ transformers: [
58
+ Plugin.FrontMatter(),
59
+ Plugin.CreatedModifiedDate({
60
+ priority: ["frontmatter", "filesystem"],
61
+ }),
62
+ CustomHeaderFooter({
63
+ baseUrl: "/edit",
64
+ position: "both",
65
+ showIcon: true,
66
+ showAuthoringGuide: true,
67
+ showHomeLink: true,
68
+ showEditInCMS: true,
69
+ }) as any, // Cast to any since we can't import Quartz types from submodule
70
+ Plugin.SyntaxHighlighting({
71
+ theme: {
72
+ light: "github-light",
73
+ dark: "github-dark",
74
+ },
75
+ keepBackground: false,
76
+ }),
77
+ Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
78
+ Plugin.GitHubFlavoredMarkdown(),
79
+ Plugin.TableOfContents({
80
+ maxDepth: 3,
81
+ minEntries: 1,
82
+ showByDefault: true,
83
+ collapseByDefault: false,
84
+ }),
85
+ Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
86
+ Plugin.Description(),
87
+ Plugin.Latex({ renderEngine: "katex" }),
88
+ ],
89
+ filters: [Plugin.RemoveDrafts()],
90
+ emitters: [
91
+ Plugin.AliasRedirects(),
92
+ Plugin.ComponentResources(),
93
+ Plugin.ContentPage(),
94
+ Plugin.FolderPage(),
95
+ Plugin.TagPage(),
96
+ Plugin.ContentIndex({
97
+ enableSiteMap: true,
98
+ enableRSS: true,
99
+ }),
100
+ Plugin.Assets(),
101
+ Plugin.Static(),
102
+ Plugin.NotFoundPage(),
103
+ ],
104
+ },
105
+ }
106
+
107
+ export default config
@@ -0,0 +1,124 @@
1
+ import { PageLayout, SharedLayout } from "./quartz/cfg"
2
+ import * as Component from "./quartz/components"
3
+
4
+ /**
5
+ * Quartz Layout Configuration for Synapse Documentation Framework
6
+ *
7
+ * This file configures the positioning of graph, backlinks, and tags components
8
+ * as required by SYN-P1-T02.
9
+ */
10
+
11
+ // Components shared across all pages
12
+ export const sharedPageComponents: SharedLayout = {
13
+ head: Component.Head(),
14
+ header: [],
15
+ afterBody: [],
16
+ footer: Component.Footer({
17
+ links: {
18
+ "GitHub": "https://github.com/your-org/synapse",
19
+ "Obsidian": "obsidian://open?vault=synapse",
20
+ },
21
+ }),
22
+ }
23
+
24
+ // Layout for list pages (folders, tags, etc.)
25
+ export const defaultListPageLayout: PageLayout = {
26
+ beforeBody: [
27
+ Component.Breadcrumbs(),
28
+ Component.ArticleTitle(),
29
+ Component.ContentMeta(),
30
+ ],
31
+ left: [
32
+ Component.PageTitle(),
33
+ Component.MobileOnly(Component.Spacer()),
34
+ Component.Search(),
35
+ Component.Darkmode(),
36
+ Component.DesktopOnly(Component.Explorer()),
37
+ ],
38
+ right: [
39
+ Component.DesktopOnly(Component.Graph({
40
+ localGraph: {
41
+ drag: true,
42
+ zoom: true,
43
+ depth: 2,
44
+ scale: 1.1,
45
+ repelForce: 0.5,
46
+ centerForce: 0.3,
47
+ linkDistance: 30,
48
+ fontSize: 0.6,
49
+ opacityScale: 1,
50
+ showTags: true,
51
+ removeTags: [],
52
+ focusOnHover: true,
53
+ },
54
+ globalGraph: {
55
+ drag: true,
56
+ zoom: true,
57
+ depth: -1,
58
+ scale: 0.9,
59
+ repelForce: 0.5,
60
+ centerForce: 0.3,
61
+ linkDistance: 30,
62
+ fontSize: 0.5,
63
+ opacityScale: 1,
64
+ showTags: true,
65
+ removeTags: [],
66
+ focusOnHover: true,
67
+ },
68
+ })),
69
+ Component.DesktopOnly(Component.TableOfContents()),
70
+ ],
71
+ }
72
+
73
+ // Layout for individual content pages
74
+ export const defaultContentPageLayout: PageLayout = {
75
+ beforeBody: [
76
+ Component.Breadcrumbs(),
77
+ Component.ArticleTitle(),
78
+ Component.ContentMeta(),
79
+ Component.TagList(), // Tags displayed prominently on content pages
80
+ ],
81
+ left: [
82
+ Component.PageTitle(),
83
+ Component.MobileOnly(Component.Spacer()),
84
+ Component.Search(),
85
+ Component.Darkmode(),
86
+ Component.DesktopOnly(Component.Explorer()),
87
+ ],
88
+ right: [
89
+ // Interactive graph showing connections
90
+ Component.Graph({
91
+ localGraph: {
92
+ drag: true,
93
+ zoom: true,
94
+ depth: 2,
95
+ scale: 1.1,
96
+ repelForce: 0.5,
97
+ centerForce: 0.3,
98
+ linkDistance: 30,
99
+ fontSize: 0.6,
100
+ opacityScale: 1,
101
+ showTags: true,
102
+ removeTags: [],
103
+ focusOnHover: true,
104
+ },
105
+ globalGraph: {
106
+ drag: true,
107
+ zoom: true,
108
+ depth: -1,
109
+ scale: 0.9,
110
+ repelForce: 0.5,
111
+ centerForce: 0.3,
112
+ linkDistance: 30,
113
+ fontSize: 0.5,
114
+ opacityScale: 1,
115
+ showTags: true,
116
+ removeTags: [],
117
+ focusOnHover: true,
118
+ },
119
+ }),
120
+ Component.DesktopOnly(Component.TableOfContents()),
121
+ // Automatic backlinks showing documents that reference this one
122
+ Component.Backlinks(),
123
+ ],
124
+ }