@dmthepm/commune 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.
- package/LICENSE +21 -0
- package/README.md +468 -0
- package/bin/commune.mjs +29 -0
- package/lib/cli/check.d.ts +13 -0
- package/lib/cli/check.js +58 -0
- package/lib/cli/errors.d.ts +29 -0
- package/lib/cli/errors.js +41 -0
- package/lib/cli/gate.d.ts +34 -0
- package/lib/cli/gate.js +165 -0
- package/lib/cli/main.d.ts +20 -0
- package/lib/cli/main.js +175 -0
- package/lib/cli/query.d.ts +30 -0
- package/lib/cli/query.js +103 -0
- package/lib/cli/related.d.ts +17 -0
- package/lib/cli/related.js +177 -0
- package/lib/cli/render.d.ts +20 -0
- package/lib/cli/render.js +32 -0
- package/lib/cli/root.d.ts +11 -0
- package/lib/cli/root.js +28 -0
- package/lib/cli/usage.d.ts +3 -0
- package/lib/cli/usage.js +46 -0
- package/lib/cli/version.d.ts +24 -0
- package/lib/cli/version.js +29 -0
- package/lib/integration.d.ts +24 -0
- package/lib/integration.js +111 -0
- package/lib/lib/graph.d.ts +354 -0
- package/lib/lib/graph.js +774 -0
- package/lib/markdown.d.ts +30 -0
- package/lib/markdown.js +24 -0
- package/lib/rehype-external-links.d.ts +15 -0
- package/lib/rehype-external-links.js +46 -0
- package/lib/remark-wikilinks.d.ts +25 -0
- package/lib/remark-wikilinks.js +108 -0
- package/package.json +101 -0
- package/src/components/Backlinks.astro +17 -0
- package/src/components/BacklinksScript.astro +117 -0
- package/src/components/Footer.astro +35 -0
- package/src/components/Header.astro +250 -0
- package/src/components/HeaderStarScript.astro +380 -0
- package/src/components/HomeFooterCards.astro +155 -0
- package/src/components/MarkdownLink.astro +36 -0
- package/src/components/PlausibleScript.astro +13 -0
- package/src/components/RelatedNotes.astro +77 -0
- package/src/components/SearchModal.astro +213 -0
- package/src/components/StarredLinksScript.astro +92 -0
- package/src/components/panes.ts +61 -0
- package/src/styles/design-system.css +157 -0
- package/src/styles/notes.css +85 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Homepage Footer Cards
|
|
4
|
+
* Two-column layout showing backlinks and recent activity
|
|
5
|
+
* Only displays on the home note (My Working Notes)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
slug: string;
|
|
10
|
+
backlinks?: Array<{
|
|
11
|
+
slug: string;
|
|
12
|
+
title: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { slug, backlinks = [] } = Astro.props;
|
|
17
|
+
|
|
18
|
+
// Only show on home note
|
|
19
|
+
const isHomePage = slug === '/notes/my-working-notes/';
|
|
20
|
+
if (!isHomePage) return null;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
{isHomePage && (
|
|
24
|
+
<section class="home-footer-cards">
|
|
25
|
+
<div class="footer-cards-container">
|
|
26
|
+
|
|
27
|
+
<!-- Left Card: Backlinks -->
|
|
28
|
+
<div class="footer-card backlinks-card">
|
|
29
|
+
<h3 class="card-title">Notes linking here</h3>
|
|
30
|
+
<div class="card-content">
|
|
31
|
+
{backlinks.length === 0 ? (
|
|
32
|
+
<p class="empty-state">No backlinks yet</p>
|
|
33
|
+
) : (
|
|
34
|
+
<ul class="backlinks-list">
|
|
35
|
+
{backlinks.slice(0, 5).map((link) => (
|
|
36
|
+
<li>
|
|
37
|
+
<a href={link.slug}>{link.title}</a>
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
)}
|
|
42
|
+
{backlinks.length > 5 && (
|
|
43
|
+
<button class="expand-button">
|
|
44
|
+
View all {backlinks.length} links →
|
|
45
|
+
</button>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Right Card: Recent Activity -->
|
|
51
|
+
<div class="footer-card activity-card">
|
|
52
|
+
<h3 class="card-title">Recent work</h3>
|
|
53
|
+
<div class="card-content">
|
|
54
|
+
<p class="empty-state">Commit history analysis coming soon</p>
|
|
55
|
+
<!-- Will be populated with LLM-analyzed commit summaries -->
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
.home-footer-cards {
|
|
65
|
+
margin-top: 4rem;
|
|
66
|
+
padding-top: 3rem;
|
|
67
|
+
border-top: 1px solid var(--c-border);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.footer-cards-container {
|
|
71
|
+
display: grid;
|
|
72
|
+
grid-template-columns: 1fr 1fr;
|
|
73
|
+
gap: 2rem;
|
|
74
|
+
max-width: 100%;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Mobile: Stack vertically */
|
|
78
|
+
@media (max-width: 767px) {
|
|
79
|
+
.footer-cards-container {
|
|
80
|
+
grid-template-columns: 1fr;
|
|
81
|
+
gap: 1.5rem;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.footer-card {
|
|
86
|
+
background: var(--c-bg-soft);
|
|
87
|
+
border: 1px solid var(--c-border);
|
|
88
|
+
border-radius: var(--c-radius-md);
|
|
89
|
+
padding: 1.5rem;
|
|
90
|
+
min-height: 200px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.card-title {
|
|
94
|
+
font-size: 1.1rem;
|
|
95
|
+
font-weight: 600;
|
|
96
|
+
margin: 0 0 1rem 0;
|
|
97
|
+
color: var(--c-text);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.card-content {
|
|
101
|
+
font-size: 0.95rem;
|
|
102
|
+
line-height: 1.6;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.backlinks-list {
|
|
106
|
+
list-style: none;
|
|
107
|
+
padding: 0;
|
|
108
|
+
margin: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.backlinks-list li {
|
|
112
|
+
margin-bottom: 0.75rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.backlinks-list a {
|
|
116
|
+
color: var(--c-text-accent);
|
|
117
|
+
text-decoration: none;
|
|
118
|
+
border-bottom: 1px solid transparent;
|
|
119
|
+
transition: all 0.15s ease;
|
|
120
|
+
display: block;
|
|
121
|
+
padding: 0.5rem;
|
|
122
|
+
margin: -0.5rem;
|
|
123
|
+
border-radius: var(--c-radius-sm);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.backlinks-list a:hover {
|
|
127
|
+
background: var(--c-bg);
|
|
128
|
+
border-bottom-color: var(--c-accent);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.empty-state {
|
|
132
|
+
color: var(--c-text-muted);
|
|
133
|
+
font-style: italic;
|
|
134
|
+
margin: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.expand-button {
|
|
138
|
+
margin-top: 1rem;
|
|
139
|
+
background: transparent;
|
|
140
|
+
border: 1px solid var(--c-border);
|
|
141
|
+
color: var(--c-text-accent);
|
|
142
|
+
padding: 0.5rem 1rem;
|
|
143
|
+
border-radius: var(--c-radius-sm);
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
font-size: 0.9rem;
|
|
146
|
+
transition: all 0.15s ease;
|
|
147
|
+
width: 100%;
|
|
148
|
+
text-align: center;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.expand-button:hover {
|
|
152
|
+
background: var(--c-bg);
|
|
153
|
+
border-color: var(--c-accent);
|
|
154
|
+
}
|
|
155
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* "view as markdown" — the source-file affordance on every rendered entry.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately one line of text: the real interface is the URL convention
|
|
6
|
+
* (append `.md` to any page), and this link is only the thing that teaches a
|
|
7
|
+
* reader the convention exists. A card or an icon would sell it as a feature.
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
href: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { href } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
<p class="markdown-link"><a href={href}>view as markdown</a></p>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
.markdown-link {
|
|
19
|
+
margin: 2.5rem 0 0;
|
|
20
|
+
font-size: 0.9rem;
|
|
21
|
+
color: var(--c-text-muted);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.markdown-link a {
|
|
25
|
+
color: var(--c-text-muted);
|
|
26
|
+
text-decoration: none;
|
|
27
|
+
border-bottom: 1px solid var(--c-border);
|
|
28
|
+
padding-bottom: 1px;
|
|
29
|
+
transition: all 0.15s ease;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.markdown-link a:hover {
|
|
33
|
+
color: var(--c-accent);
|
|
34
|
+
border-bottom-color: var(--c-accent);
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Plausible Analytics Script Component
|
|
3
|
+
// Privacy-friendly analytics with no cookies or personal data collection
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
domain: string;
|
|
7
|
+
src?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { domain, src = "https://analytics.devonmeadows.com/js/script.js" } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<script defer data-domain={domain} src={src}></script>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Related Notes component - finds notes with overlapping tags.
|
|
4
|
+
*
|
|
5
|
+
* This helps discovery: "if you're interested in this, you might
|
|
6
|
+
* also like these other notes."
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getCollection } from 'astro:content';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
slug: string;
|
|
13
|
+
tags: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { slug, tags } = Astro.props;
|
|
17
|
+
|
|
18
|
+
// Get all public notes
|
|
19
|
+
const allNotes = await getCollection('notes', ({ data }) => data.visibility === 'public');
|
|
20
|
+
|
|
21
|
+
// Find notes with overlapping tags (excluding current note)
|
|
22
|
+
const related = allNotes
|
|
23
|
+
.filter((note) => note.id !== slug)
|
|
24
|
+
.map((note) => {
|
|
25
|
+
const overlap = note.data.tags.filter((tag) => tags.includes(tag));
|
|
26
|
+
return {
|
|
27
|
+
slug: note.id,
|
|
28
|
+
title: note.data.title,
|
|
29
|
+
tags: note.data.tags,
|
|
30
|
+
overlapCount: overlap.length,
|
|
31
|
+
};
|
|
32
|
+
})
|
|
33
|
+
.filter((note) => note.overlapCount > 0)
|
|
34
|
+
.sort((a, b) => b.overlapCount - a.overlapCount)
|
|
35
|
+
.slice(0, 8); // Show top 8
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
{related.length > 0 && (
|
|
39
|
+
<section class="related-notes">
|
|
40
|
+
<h3 class="text-sm font-semibold tracking-wide uppercase text-gray-500 dark:text-gray-400 mb-4">
|
|
41
|
+
Related Notes
|
|
42
|
+
</h3>
|
|
43
|
+
<ul class="space-y-3">
|
|
44
|
+
{related.map(({ slug: relSlug, title, tags: relTags }) => (
|
|
45
|
+
<li>
|
|
46
|
+
<a
|
|
47
|
+
href={`/notes/${relSlug}/`}
|
|
48
|
+
class="block group"
|
|
49
|
+
>
|
|
50
|
+
<div class="text-sm text-[var(--sl-color-accent)] group-hover:underline group-hover:text-[var(--sl-color-accent-high)]">
|
|
51
|
+
{title}
|
|
52
|
+
</div>
|
|
53
|
+
<div class="flex flex-wrap gap-1.5 mt-1">
|
|
54
|
+
{relTags.slice(0, 3).map((tag) => (
|
|
55
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
56
|
+
#{tag}
|
|
57
|
+
</span>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</a>
|
|
61
|
+
</li>
|
|
62
|
+
))}
|
|
63
|
+
</ul>
|
|
64
|
+
</section>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{related.length === 0 && (
|
|
68
|
+
<section class="related-notes">
|
|
69
|
+
<h3 class="text-sm font-semibold tracking-wide uppercase text-gray-500 dark:text-gray-400 mb-4">
|
|
70
|
+
Related Notes
|
|
71
|
+
</h3>
|
|
72
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
73
|
+
No related notes yet. Try adding more tags!
|
|
74
|
+
</p>
|
|
75
|
+
</section>
|
|
76
|
+
)}
|
|
77
|
+
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Result { url: string; title: string; excerpt?: string; score?: number; source?: 'pagefind'|'semantic'; }
|
|
3
|
+
---
|
|
4
|
+
<div id="commune-search" hidden>
|
|
5
|
+
<div class="shade" onclick="window.CommuneSearch.close()"></div>
|
|
6
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
7
|
+
<input id="q" placeholder="Search notes & docs… (Esc to close)" autofocus />
|
|
8
|
+
<ul id="results"></ul>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
#commune-search[hidden]{display:none}
|
|
14
|
+
.shade{
|
|
15
|
+
position:fixed;
|
|
16
|
+
inset:0;
|
|
17
|
+
background:rgba(0,0,0,.5);
|
|
18
|
+
backdrop-filter:blur(4px);
|
|
19
|
+
z-index:var(--z-modal-shade);
|
|
20
|
+
}
|
|
21
|
+
.modal{
|
|
22
|
+
position:fixed;
|
|
23
|
+
inset:auto;
|
|
24
|
+
left:50%;
|
|
25
|
+
top:12vh;
|
|
26
|
+
transform:translateX(-50%);
|
|
27
|
+
width:min(720px,90vw);
|
|
28
|
+
background:var(--c-bg);
|
|
29
|
+
border:1px solid var(--c-border);
|
|
30
|
+
border-radius:var(--c-radius-lg);
|
|
31
|
+
padding:1.25rem;
|
|
32
|
+
box-shadow:var(--c-shadow-xl);
|
|
33
|
+
z-index:var(--z-modal);
|
|
34
|
+
box-sizing:border-box;
|
|
35
|
+
}
|
|
36
|
+
#q{
|
|
37
|
+
width:100%;
|
|
38
|
+
font-size:1.1rem;
|
|
39
|
+
padding:.85rem 1.1rem;
|
|
40
|
+
border-radius:var(--c-radius-md);
|
|
41
|
+
border:1.5px solid var(--c-border);
|
|
42
|
+
outline:none;
|
|
43
|
+
box-sizing:border-box;
|
|
44
|
+
background:var(--c-bg);
|
|
45
|
+
color:var(--c-text);
|
|
46
|
+
transition:border-color 0.15s ease;
|
|
47
|
+
}
|
|
48
|
+
#q:focus{
|
|
49
|
+
border-color:var(--c-accent);
|
|
50
|
+
}
|
|
51
|
+
#results{
|
|
52
|
+
list-style:none;
|
|
53
|
+
margin:1rem 0 .5rem;
|
|
54
|
+
padding:0;
|
|
55
|
+
max-height:60vh;
|
|
56
|
+
overflow:auto;
|
|
57
|
+
}
|
|
58
|
+
#results li{
|
|
59
|
+
padding:.85rem .75rem;
|
|
60
|
+
border-radius:var(--c-radius-sm);
|
|
61
|
+
cursor:pointer;
|
|
62
|
+
transition:background 0.1s ease;
|
|
63
|
+
}
|
|
64
|
+
#results li:hover{
|
|
65
|
+
background:var(--c-bg-soft);
|
|
66
|
+
}
|
|
67
|
+
#results a{
|
|
68
|
+
text-decoration:none;
|
|
69
|
+
color:var(--c-text);
|
|
70
|
+
font-weight:600;
|
|
71
|
+
font-size:1rem;
|
|
72
|
+
}
|
|
73
|
+
#results .excerpt{
|
|
74
|
+
font-size:.9rem;
|
|
75
|
+
color:var(--c-text-muted);
|
|
76
|
+
margin-top:.35rem;
|
|
77
|
+
line-height:1.5;
|
|
78
|
+
}
|
|
79
|
+
.empty{
|
|
80
|
+
text-align:center;
|
|
81
|
+
padding:2rem;
|
|
82
|
+
color:var(--c-text-muted);
|
|
83
|
+
font-size:.95rem;
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
86
|
+
|
|
87
|
+
<script is:inline>
|
|
88
|
+
// Lightweight RRF combiner
|
|
89
|
+
const rrf = (lists) => {
|
|
90
|
+
const map = new Map();
|
|
91
|
+
lists.forEach((arr, idx) => arr.forEach((r, i) => {
|
|
92
|
+
const key = r.url;
|
|
93
|
+
const cur = map.get(key) ?? { ...r, score: 0 };
|
|
94
|
+
cur.score += 1 / (60 + i);
|
|
95
|
+
map.set(key, cur);
|
|
96
|
+
}));
|
|
97
|
+
return Array.from(map.values()).sort((a,b) => b.score - a.score);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Pagefind removed - semantic search will handle full-text needs
|
|
101
|
+
// If we ever need dedicated lexical full-text search, we can re-add it
|
|
102
|
+
|
|
103
|
+
async function doSemantic(query){
|
|
104
|
+
try{
|
|
105
|
+
const r = await fetch(`/api/ask?q=${encodeURIComponent(query)}`);
|
|
106
|
+
if (!r.ok) return [];
|
|
107
|
+
const j = await r.json();
|
|
108
|
+
return (j.results ?? []).slice(0,10).map(x => ({ url: x.url, title: x.title, excerpt: x.snippet, source:'semantic' }));
|
|
109
|
+
}catch{ return []; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// DEV fallback: use backlinks graph as minimal search index
|
|
113
|
+
async function devIndex() {
|
|
114
|
+
try {
|
|
115
|
+
const r = await fetch('/backlinks.json', { cache: 'no-store' });
|
|
116
|
+
if (!r.ok) return [];
|
|
117
|
+
const graph = await r.json();
|
|
118
|
+
return Object.entries(graph).map(([url, node]) => ({
|
|
119
|
+
url,
|
|
120
|
+
title: node.title || url,
|
|
121
|
+
excerpt: (node.summary || '').slice(0, 160),
|
|
122
|
+
// Aliases and tags are searchable but never displayed — they let you
|
|
123
|
+
// find a note by a name it answers to rather than the one it wears.
|
|
124
|
+
terms: [...(node.aliases || []), ...(node.tags || [])].join(' '),
|
|
125
|
+
}));
|
|
126
|
+
} catch { return []; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function doDevSearch(q) {
|
|
130
|
+
const idx = await devIndex();
|
|
131
|
+
if (!q) return [];
|
|
132
|
+
const norm = q.toLowerCase();
|
|
133
|
+
const scored = idx.map(d => {
|
|
134
|
+
const hay = `${d.title} ${d.excerpt} ${d.terms}`.toLowerCase();
|
|
135
|
+
const pos = hay.indexOf(norm);
|
|
136
|
+
return pos === -1 ? null : { ...d, score: 1 / (1 + pos) };
|
|
137
|
+
}).filter(Boolean).sort((a,b) => b.score - a.score);
|
|
138
|
+
return scored.slice(0, 20).map(d => ({ url: d.url, title: d.title, excerpt: d.excerpt, source: 'pagefind' }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Wire up modal
|
|
142
|
+
(function mountSearch(){
|
|
143
|
+
const root = document.getElementById('commune-search');
|
|
144
|
+
const input = root.querySelector('#q');
|
|
145
|
+
const list = root.querySelector('#results');
|
|
146
|
+
|
|
147
|
+
function render(results) {
|
|
148
|
+
if(!results.length){
|
|
149
|
+
list.innerHTML = '<div class="empty">No results found</div>';
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
list.innerHTML = results.map(r => (
|
|
153
|
+
`<li>
|
|
154
|
+
<a href="${r.url}">${r.title}</a>
|
|
155
|
+
${r.excerpt ? `<div class="excerpt">${r.excerpt}</div>` : ''}
|
|
156
|
+
</li>`
|
|
157
|
+
)).join('');
|
|
158
|
+
// Wire close on click and enter-to-go
|
|
159
|
+
list.querySelectorAll('a').forEach(a => a.onclick = () => window.CommuneSearch.close());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// SEARCH STRATEGY (2-tier hybrid):
|
|
163
|
+
// 1. Substring (title/summary) - Fast exact matching on backlinks.json
|
|
164
|
+
// 2. Semantic (Qdrant) - Conceptual similarity, foundation for "Ask the Brain"
|
|
165
|
+
// RRF combines both for best results
|
|
166
|
+
|
|
167
|
+
let t;
|
|
168
|
+
async function search(q) {
|
|
169
|
+
const [substring, semantic] = await Promise.all([
|
|
170
|
+
doDevSearch(q), // Tier 1: Exact phrase matching
|
|
171
|
+
doSemantic(q).catch(() => []), // Tier 2: Conceptual (when API ready)
|
|
172
|
+
]);
|
|
173
|
+
let merged = rrf([substring, semantic]);
|
|
174
|
+
render(merged);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function open() {
|
|
178
|
+
root.hidden = false;
|
|
179
|
+
document.body.classList.add('modal-open');
|
|
180
|
+
input.value = '';
|
|
181
|
+
render([]);
|
|
182
|
+
input.focus();
|
|
183
|
+
}
|
|
184
|
+
function close() {
|
|
185
|
+
root.hidden = true;
|
|
186
|
+
document.body.classList.remove('modal-open');
|
|
187
|
+
}
|
|
188
|
+
window.CommuneSearch = { open, close };
|
|
189
|
+
|
|
190
|
+
addEventListener('commune:openSearch', open);
|
|
191
|
+
addEventListener('keydown', e => {
|
|
192
|
+
const mac = navigator.platform.toUpperCase().includes('MAC');
|
|
193
|
+
if ((mac ? e.metaKey : e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
194
|
+
e.preventDefault(); open();
|
|
195
|
+
}
|
|
196
|
+
if (e.key === 'Escape' && !root.hidden) close();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
input.addEventListener('input', () => {
|
|
200
|
+
clearTimeout(t);
|
|
201
|
+
const q = input.value.trim();
|
|
202
|
+
t = setTimeout(() => search(q), 200);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Enter → first result
|
|
206
|
+
input.addEventListener('keydown', (e) => {
|
|
207
|
+
if (e.key === 'Enter') {
|
|
208
|
+
const first = list.querySelector('li a');
|
|
209
|
+
if (first) window.location.href = first.getAttribute('href');
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
})();
|
|
213
|
+
</script>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script is:inline>
|
|
2
|
+
// === STARRED LINKS INITIALIZATION ===
|
|
3
|
+
// Adds ⭐ stars to links pointing to starred notes
|
|
4
|
+
|
|
5
|
+
(function() {
|
|
6
|
+
let backlinksData = null;
|
|
7
|
+
|
|
8
|
+
async function initializeStarredLinks(scope = document) {
|
|
9
|
+
try {
|
|
10
|
+
// Fetch backlinks data if not already loaded
|
|
11
|
+
if (!backlinksData) {
|
|
12
|
+
const res = await fetch('/backlinks.json');
|
|
13
|
+
if (res.ok) {
|
|
14
|
+
backlinksData = await res.json();
|
|
15
|
+
} else {
|
|
16
|
+
console.warn('Failed to load backlinks.json for starred links');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Find all links to /notes/ within the scope, excluding backlinks section
|
|
22
|
+
// (BacklinksScript handles those separately)
|
|
23
|
+
const allNoteLinks = scope.querySelectorAll('a[href^="/notes/"]');
|
|
24
|
+
const noteLinks = Array.from(allNoteLinks).filter(link => !link.closest('.backlinks'));
|
|
25
|
+
|
|
26
|
+
noteLinks.forEach(link => {
|
|
27
|
+
const href = link.getAttribute('href');
|
|
28
|
+
if (!href) return;
|
|
29
|
+
|
|
30
|
+
// Normalize href to match backlinks.json format (ensure trailing slash)
|
|
31
|
+
const normalizedHref = href.endsWith('/') ? href : href + '/';
|
|
32
|
+
const note = backlinksData[normalizedHref];
|
|
33
|
+
|
|
34
|
+
if (note?.isStarred) {
|
|
35
|
+
// Check if star isn't already there
|
|
36
|
+
if (!link.querySelector('.star-indicator-inline') && !link.nextElementSibling?.classList?.contains('star-indicator-inline')) {
|
|
37
|
+
// Create wrapper to prevent line break between link and star
|
|
38
|
+
const wrapper = document.createElement('span');
|
|
39
|
+
wrapper.style.whiteSpace = 'nowrap';
|
|
40
|
+
wrapper.style.display = 'inline';
|
|
41
|
+
|
|
42
|
+
// Create star as a separate clickable element (not part of the link)
|
|
43
|
+
const star = document.createElement('span');
|
|
44
|
+
star.className = 'star-indicator-inline';
|
|
45
|
+
star.textContent = ' ⭐';
|
|
46
|
+
star.setAttribute('aria-label', 'Top 5% most linked - click to learn more');
|
|
47
|
+
star.setAttribute('role', 'button');
|
|
48
|
+
star.setAttribute('tabindex', '0');
|
|
49
|
+
|
|
50
|
+
// Click handler to open modal (not navigate)
|
|
51
|
+
star.addEventListener('click', (e) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
e.stopPropagation();
|
|
54
|
+
if (typeof window.showStarModal === 'function') {
|
|
55
|
+
window.showStarModal();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Keyboard support
|
|
60
|
+
star.addEventListener('keydown', (e) => {
|
|
61
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
if (typeof window.showStarModal === 'function') {
|
|
65
|
+
window.showStarModal();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Wrap the link in the nowrap container
|
|
71
|
+
link.parentNode.insertBefore(wrapper, link);
|
|
72
|
+
wrapper.appendChild(link);
|
|
73
|
+
wrapper.appendChild(star);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error initializing starred links:', error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Make it globally available for dynamic pane loading
|
|
83
|
+
window.initializeStarredLinks = initializeStarredLinks;
|
|
84
|
+
|
|
85
|
+
// Auto-initialize on DOM ready
|
|
86
|
+
if (document.readyState === 'loading') {
|
|
87
|
+
document.addEventListener('DOMContentLoaded', () => initializeStarredLinks());
|
|
88
|
+
} else {
|
|
89
|
+
initializeStarredLinks();
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function setupPanes(){
|
|
2
|
+
const stack = document.getElementById('pane-stack');
|
|
3
|
+
if(!stack) return;
|
|
4
|
+
|
|
5
|
+
function activateStack(on: boolean){
|
|
6
|
+
stack.classList.toggle('active', on);
|
|
7
|
+
document.body.classList.toggle('panes-open', on);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function openPane(url: string){
|
|
11
|
+
activateStack(true);
|
|
12
|
+
|
|
13
|
+
const res = await fetch(url, { headers: { 'Accept':'text/html;charset=utf-8' } });
|
|
14
|
+
const html = await res.text();
|
|
15
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
16
|
+
const main = doc.querySelector('main') ?? doc.body;
|
|
17
|
+
|
|
18
|
+
const pane = document.createElement('div');
|
|
19
|
+
pane.className = 'pane';
|
|
20
|
+
pane.innerHTML = `
|
|
21
|
+
<header><strong>${doc.title?.replace(/ \|.*/,'')}</strong>
|
|
22
|
+
<button class="close" aria-label="Close">×</button>
|
|
23
|
+
</header>
|
|
24
|
+
<div class="body"><article class="prose">${main.innerHTML}</article></div>`;
|
|
25
|
+
|
|
26
|
+
pane.querySelector<HTMLButtonElement>('.close')!.addEventListener('click', () => {
|
|
27
|
+
pane.remove();
|
|
28
|
+
if(!stack.querySelector('.pane')) activateStack(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
stack.appendChild(pane);
|
|
32
|
+
stack.scrollLeft = stack.scrollWidth;
|
|
33
|
+
|
|
34
|
+
// Limit to 3
|
|
35
|
+
const panes = stack.querySelectorAll('.pane');
|
|
36
|
+
if(panes.length > 3) panes[0].remove();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Only intercept clicks inside the main article area
|
|
40
|
+
document.addEventListener('click', (e) => {
|
|
41
|
+
const inArticle = (e.target as HTMLElement)?.closest?.('main, article.prose');
|
|
42
|
+
if (!inArticle) return;
|
|
43
|
+
|
|
44
|
+
const a = (e.target as HTMLElement)?.closest?.('a[href^="/notes/"]') as HTMLAnchorElement | null;
|
|
45
|
+
if(!a) return;
|
|
46
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey) return; // allow new tab, etc.
|
|
47
|
+
if (window.innerWidth < 1024) return; // mobile → normal nav
|
|
48
|
+
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
openPane(a.href);
|
|
51
|
+
history.pushState({ pane: a.href }, '', a.href);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
addEventListener('popstate', () => {
|
|
55
|
+
const panes = stack.querySelectorAll('.pane');
|
|
56
|
+
if(panes.length){
|
|
57
|
+
panes[panes.length-1].remove();
|
|
58
|
+
if(!stack.querySelector('.pane')) activateStack(false);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|