@acms-subir/core 0.0.2 → 0.2.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/CHANGELOG.md +75 -0
- package/cli.mjs +128 -16
- package/integration.mjs +96 -3
- package/package.json +14 -12
- package/src/actions/index.ts +184 -6
- package/src/components/DesignManager.tsx +53 -17
- package/src/components/LinksManager.tsx +110 -48
- package/src/components/NavManager.tsx +83 -37
- package/src/components/editor/PostEditor.tsx +228 -55
- package/src/components/editor/extensions/cards.tsx +17 -1
- package/src/components/editor/extensions/codeBlock.tsx +1 -1
- package/src/components/editor/extensions/infoCards.tsx +16 -1
- package/src/components/editor/extensions/mediaCards.tsx +36 -2
- package/src/components/editor/extensions/slash.tsx +70 -8
- package/src/components/editor/extensions/toc.tsx +89 -0
- package/src/components/editor/extensions/toggle.tsx +70 -0
- package/src/layouts/Site.astro +122 -1
- package/src/lib/analytics.ts +121 -0
- package/src/lib/designPreview.ts +14 -0
- package/src/lib/drafts.ts +77 -0
- package/src/lib/dynamicBlocks.ts +43 -3
- package/src/lib/plaintext.ts +22 -0
- package/src/lib/repo/migrations.ts +52 -0
- package/src/lib/repo/sql.ts +289 -30
- package/src/lib/repo/types.ts +40 -4
- package/src/lib/schema.ts +53 -0
- package/src/lib/search-config.ts +10 -0
- package/src/lib/search-i18n.ts +45 -0
- package/src/pages/[...slug].astro +3 -1
- package/src/pages/admin/c/[collection]/[id].astro +10 -3
- package/src/pages/admin/c/[collection]/index.astro +1 -0
- package/src/pages/admin/design.astro +6 -1
- package/src/pages/admin/index.astro +27 -10
- package/src/pages/admin/pages/[id].astro +10 -2
- package/src/pages/admin/pages/index.astro +1 -0
- package/src/pages/admin/settings.astro +148 -0
- package/src/pages/api/search.ts +44 -0
- package/src/pages/blog/[slug].astro +4 -1
- package/src/pages/index.astro +4 -1
- package/src/pages/links.astro +10 -4
- package/src/pages/preview/[type]/[id].astro +9 -4
- package/src/pages/search.astro +83 -18
- package/src/styles/admin.css +4 -0
- package/src/styles/cards.css +23 -0
- package/src/styles/editor.css +53 -1
- package/src/styles/site.css +88 -0
- package/src/lib/repo/search.ts +0 -14
package/src/pages/search.astro
CHANGED
|
@@ -1,41 +1,54 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Site from '../layouts/Site.astro';
|
|
3
3
|
import { getRepo } from '../lib/repo';
|
|
4
|
-
import { formatDate } from '../lib/dates';
|
|
5
|
-
import {
|
|
4
|
+
import { formatDate, siteLang } from '../lib/dates';
|
|
5
|
+
import { collectionUrl, tagUrl } from '../lib/content';
|
|
6
|
+
import { authorSlug } from '../lib/schema';
|
|
7
|
+
import { I18N, localeFromPath, localeHomeUrl, normalizePageLocale } from '../lib/i18n';
|
|
8
|
+
import { searchStrings } from '../lib/search-i18n';
|
|
9
|
+
|
|
10
|
+
// The no-JS search surface (the modal's form falls back here). Serves both
|
|
11
|
+
// /search and the locale-prefixed /<locale>/search routes — the locale comes
|
|
12
|
+
// from the path, same trick as the per-locale home routes, and only boosts
|
|
13
|
+
// page ranking; every locale's content stays visible.
|
|
6
14
|
|
|
7
15
|
const repo = await getRepo(Astro.locals);
|
|
8
16
|
const q = (Astro.url.searchParams.get('q') ?? '').trim();
|
|
9
|
-
const results = q ? await repo.searchPublished(q) : { posts: [], pages: [] };
|
|
10
17
|
const settings = await repo.getSettings();
|
|
11
|
-
const
|
|
18
|
+
const reqLocale = I18N.enabled ? localeFromPath(Astro.url.pathname) : '';
|
|
19
|
+
const locale = normalizePageLocale(reqLocale);
|
|
20
|
+
const t = searchStrings((reqLocale || siteLang(settings)).slice(0, 2));
|
|
21
|
+
const results = q
|
|
22
|
+
? await repo.searchPublished(q, { locale })
|
|
23
|
+
: { posts: [], pages: [], tags: [], authors: [] };
|
|
24
|
+
const total = results.posts.length + results.pages.length + results.tags.length + results.authors.length;
|
|
12
25
|
---
|
|
13
26
|
|
|
14
|
-
<Site title={q ?
|
|
27
|
+
<Site title={q ? `${t.label}: ${q}` : t.label} description={`${t.label} · ${settings.siteTitle}`} noindex={Boolean(q)}>
|
|
15
28
|
<header class="search-hero">
|
|
16
|
-
<h1>
|
|
17
|
-
<form class="search-form" action=
|
|
29
|
+
<h1>{t.label}</h1>
|
|
30
|
+
<form class="search-form" action={Astro.url.pathname} method="get" role="search">
|
|
18
31
|
<input
|
|
19
32
|
type="search"
|
|
20
33
|
name="q"
|
|
21
34
|
value={q}
|
|
22
|
-
placeholder={
|
|
23
|
-
aria-label=
|
|
35
|
+
placeholder={t.placeholder}
|
|
36
|
+
aria-label={t.label}
|
|
24
37
|
autofocus={!q}
|
|
25
38
|
/>
|
|
26
|
-
<button type="submit">
|
|
39
|
+
<button type="submit">{t.button}</button>
|
|
27
40
|
</form>
|
|
28
41
|
</header>
|
|
29
42
|
|
|
30
43
|
{q && (
|
|
31
44
|
<p class="search-count">
|
|
32
|
-
{total === 0 ?
|
|
45
|
+
{total === 0 ? `${t.empty} “${q}”.` : `${total} ${total === 1 ? t.result : t.results} · “${q}”`}
|
|
33
46
|
</p>
|
|
34
47
|
)}
|
|
35
48
|
|
|
36
49
|
{results.posts.length > 0 && (
|
|
37
50
|
<ul class="post-list">
|
|
38
|
-
{results.posts.map((p) => (
|
|
51
|
+
{results.posts.map(({ doc: p, snippet }) => (
|
|
39
52
|
<li class="post-item">
|
|
40
53
|
{p.cover && (
|
|
41
54
|
<a class="post-thumb" href={collectionUrl(p.collection, p.slug)}><img src={p.cover} alt={p.coverAlt} loading="lazy" /></a>
|
|
@@ -45,7 +58,11 @@ const total = results.posts.length + results.pages.length;
|
|
|
45
58
|
<div class="post-meta">
|
|
46
59
|
{p.publishedAt && <time datetime={new Date(p.publishedAt).toISOString()}>{formatDate(p.publishedAt, settings)}</time>}
|
|
47
60
|
</div>
|
|
48
|
-
{
|
|
61
|
+
{snippet ? (
|
|
62
|
+
<p class="post-excerpt" set:html={snippet} />
|
|
63
|
+
) : (
|
|
64
|
+
p.excerpt && <p class="post-excerpt">{p.excerpt}</p>
|
|
65
|
+
)}
|
|
49
66
|
</div>
|
|
50
67
|
</li>
|
|
51
68
|
))}
|
|
@@ -53,12 +70,46 @@ const total = results.posts.length + results.pages.length;
|
|
|
53
70
|
)}
|
|
54
71
|
|
|
55
72
|
{results.pages.length > 0 && (
|
|
56
|
-
<section class="search-
|
|
57
|
-
<h2>
|
|
73
|
+
<section class="search-section">
|
|
74
|
+
<h2>{t.pages}</h2>
|
|
58
75
|
<ul class="post-list">
|
|
59
|
-
{results.pages.map((p) => (
|
|
76
|
+
{results.pages.map(({ doc: p, snippet }) => (
|
|
60
77
|
<li class="post-item">
|
|
61
|
-
<div
|
|
78
|
+
<div>
|
|
79
|
+
<a href={p.isHome ? localeHomeUrl(p.locale) : `/${p.slug}`}>{p.title}</a>
|
|
80
|
+
{I18N.enabled && p.locale !== locale && (
|
|
81
|
+
<span class="search-loc">{(p.locale || I18N.defaultLocale).toUpperCase()}</span>
|
|
82
|
+
)}
|
|
83
|
+
{snippet && <p class="post-excerpt" set:html={snippet} />}
|
|
84
|
+
</div>
|
|
85
|
+
</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
</section>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{results.tags.length > 0 && (
|
|
92
|
+
<section class="search-section">
|
|
93
|
+
<h2>{t.tags}</h2>
|
|
94
|
+
<div class="search-tags">
|
|
95
|
+
{results.tags.map(({ tag, count }) => (
|
|
96
|
+
<a class="tag" href={tagUrl(tag.slug)}>{tag.name} <span class="search-tag-count">{count}</span></a>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
</section>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{results.authors.length > 0 && (
|
|
103
|
+
<section class="search-section">
|
|
104
|
+
<h2>{t.authors}</h2>
|
|
105
|
+
<ul class="post-list">
|
|
106
|
+
{results.authors.map((u) => (
|
|
107
|
+
<li class="post-item">
|
|
108
|
+
{u.avatar && <img class="search-avatar" src={u.avatar} alt="" loading="lazy" />}
|
|
109
|
+
<div>
|
|
110
|
+
<a href={`/author/${authorSlug(u)}`}>{u.name}</a>
|
|
111
|
+
{u.bio && <p class="post-excerpt">{u.bio}</p>}
|
|
112
|
+
</div>
|
|
62
113
|
</li>
|
|
63
114
|
))}
|
|
64
115
|
</ul>
|
|
@@ -79,5 +130,19 @@ const total = results.posts.length + results.pages.length;
|
|
|
79
130
|
background: var(--accent); color: #fff; cursor: pointer;
|
|
80
131
|
}
|
|
81
132
|
.search-count { color: var(--muted); }
|
|
82
|
-
.search-
|
|
133
|
+
.search-section h2 { font-size: 1.1rem; margin-top: 2rem; }
|
|
134
|
+
/* snippets arrive pre-escaped with <mark> around matches (set:html), so
|
|
135
|
+
the highlight style must reach into the injected fragment */
|
|
136
|
+
.post-excerpt :global(mark) {
|
|
137
|
+
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
138
|
+
color: inherit; border-radius: 3px; padding: 0 1px;
|
|
139
|
+
}
|
|
140
|
+
.search-loc {
|
|
141
|
+
font-size: 11px; font-weight: 700; letter-spacing: 0.06em; color: var(--muted);
|
|
142
|
+
border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; margin-left: 8px;
|
|
143
|
+
vertical-align: 2px;
|
|
144
|
+
}
|
|
145
|
+
.search-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
|
|
146
|
+
.search-tag-count { color: var(--muted); }
|
|
147
|
+
.search-avatar { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; flex: none; }
|
|
83
148
|
</style>
|
package/src/styles/admin.css
CHANGED
|
@@ -140,6 +140,8 @@ table.list { width: 100%; border-collapse: collapse; }
|
|
|
140
140
|
.pill.scheduled { background: #fde5d8; color: var(--accent-deep); }
|
|
141
141
|
.pill.archived { background: rgb(102 109 124 / 12%); color: var(--muted); }
|
|
142
142
|
.pill.home { background: #fde5d8; color: var(--accent-deep); }
|
|
143
|
+
/* live doc with staged edits awaiting Update */
|
|
144
|
+
.pill.edited { background: var(--amber-soft); color: var(--amber); margin-left: 4px; }
|
|
143
145
|
.pill.lang { background: var(--panel-2); color: var(--muted); border: 1px solid var(--line); }
|
|
144
146
|
|
|
145
147
|
/* buttons & forms */
|
|
@@ -443,6 +445,8 @@ input.bare.mono { font-family: ui-monospace, 'SF Mono', monospace; font-size: 12
|
|
|
443
445
|
.lm-savestate { font-size: 11.5px; color: var(--faint); margin-left: auto; }
|
|
444
446
|
.lm-savestate.saving { color: var(--muted); }
|
|
445
447
|
.lm-savestate.error { color: var(--red); }
|
|
448
|
+
/* local edits awaiting an explicit "Save changes" */
|
|
449
|
+
.lm-savestate.dirty { color: var(--amber); font-weight: 600; }
|
|
446
450
|
.lm-phone {
|
|
447
451
|
border: 1px solid var(--line); border-radius: var(--r-xl); overflow: hidden;
|
|
448
452
|
background: #fff; height: min(640px, calc(100vh - 140px)); box-shadow: var(--shadow-pop);
|
package/src/styles/cards.css
CHANGED
|
@@ -174,3 +174,26 @@ a.pc-info-card:hover { border-color: var(--info-badge); }
|
|
|
174
174
|
.pc-info-row[data-icon='tag']::before { display: inline-block; -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 4h7l9 9-7 7-9-9zM8.5 8.5h.01'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 4h7l9 9-7 7-9-9zM8.5 8.5h.01'/%3E%3C/svg%3E"); }
|
|
175
175
|
.pc-info-row[data-icon='check']::before { display: inline-block; -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12.5l5 5 11-11'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12.5l5 5 11-11'/%3E%3C/svg%3E"); }
|
|
176
176
|
}
|
|
177
|
+
|
|
178
|
+
@layer base {
|
|
179
|
+
/* toggle — collapsible section, native <details> so no JS needed */
|
|
180
|
+
.pc-toggle { border: 1px solid var(--line, #e3e3e0); border-radius: 10px; padding: 12px 18px; margin: 1.6em 0; }
|
|
181
|
+
.pc-toggle > summary { font-weight: 600; cursor: pointer; list-style: none; display: flex; align-items: center; gap: 10px; }
|
|
182
|
+
.pc-toggle > summary::-webkit-details-marker { display: none; }
|
|
183
|
+
.pc-toggle > summary::before { content: "▸"; font-size: 0.8em; color: var(--muted, #777); transition: transform 0.15s ease; }
|
|
184
|
+
.pc-toggle[open] > summary::before { transform: rotate(90deg); }
|
|
185
|
+
.pc-toggle-body { margin-top: 10px; }
|
|
186
|
+
.pc-toggle-body > :last-child { margin-bottom: 0; }
|
|
187
|
+
|
|
188
|
+
/* table of contents — expanded at request time from the body's headings */
|
|
189
|
+
.pc-toc {
|
|
190
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
191
|
+
border-left: 3px solid var(--accent, #6e7bff); background: rgb(0 0 0 / 3%);
|
|
192
|
+
border-radius: 0 10px 10px 0; padding: 14px 20px; margin: 1.6em 0;
|
|
193
|
+
}
|
|
194
|
+
.pc-toc-title { text-transform: uppercase; letter-spacing: 0.08em; font-size: 11.5px; font-weight: 700; color: var(--muted, #777); margin-bottom: 6px; }
|
|
195
|
+
.pc-toc-link { color: inherit; text-decoration: none; padding: 2px 0; font-size: 0.95em; }
|
|
196
|
+
a.pc-toc-link:hover { color: var(--accent, #6e7bff); }
|
|
197
|
+
.pc-toc-link[data-depth='1'] { padding-left: 16px; }
|
|
198
|
+
.pc-toc-link[data-depth='2'] { padding-left: 32px; }
|
|
199
|
+
}
|
package/src/styles/editor.css
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
}
|
|
12
12
|
.pe-topbar-left, .pe-topbar-right { display: flex; align-items: center; gap: 10px; }
|
|
13
13
|
.pe-savestate { font-size: 12.5px; color: var(--faint); }
|
|
14
|
+
.pe-edited-dot { font-style: normal; font-weight: 600; color: var(--amber); }
|
|
14
15
|
.pe-savestate.dirty { color: var(--muted); }
|
|
15
16
|
.pe-savestate.saving { color: var(--muted); }
|
|
16
17
|
.pe-savestate.error { color: var(--red); }
|
|
@@ -98,6 +99,7 @@
|
|
|
98
99
|
/* prose */
|
|
99
100
|
.pe-prose { outline: none; color: var(--text); font-size: 16.5px; line-height: 1.72; min-height: 320px; }
|
|
100
101
|
.pe-prose p { margin: 0 0 0.9em; }
|
|
102
|
+
.pe-prose h1 { font-size: 31px; letter-spacing: -0.02em; margin: 1.3em 0 0.5em; }
|
|
101
103
|
.pe-prose h2 { font-size: 25px; letter-spacing: -0.015em; margin: 1.4em 0 0.5em; }
|
|
102
104
|
.pe-prose h3 { font-size: 19px; letter-spacing: -0.01em; margin: 1.3em 0 0.4em; }
|
|
103
105
|
.pe-prose a { color: var(--accent-deep); text-decoration: underline; text-decoration-color: rgb(238 93 32 / 35%); }
|
|
@@ -106,6 +108,8 @@
|
|
|
106
108
|
.pe-prose pre { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 14px 16px; overflow-x: auto; margin: 1.2em 0; }
|
|
107
109
|
.pe-prose pre code { background: none; border: none; padding: 0; font-size: 13.5px; }
|
|
108
110
|
.pe-prose hr { border: none; border-top: 1px solid var(--line); margin: 2em auto; width: 120px; }
|
|
111
|
+
.pe-prose mark { background: #fde58a; border-radius: 3px; padding: 0 2px; }
|
|
112
|
+
.pe-bubble mark { background: #fde58a; border-radius: 3px; padding: 0 4px; color: #1a1d25; }
|
|
109
113
|
|
|
110
114
|
/* ---- code block (extensions/codeBlock.tsx) ---- */
|
|
111
115
|
.pe-codeblock { position: relative; margin: 1.2em 0; }
|
|
@@ -246,6 +250,12 @@
|
|
|
246
250
|
background: none; border: none; border-radius: var(--r-md); padding: 7px 10px; cursor: pointer; font-family: inherit;
|
|
247
251
|
}
|
|
248
252
|
.pe-slash-item.is-selected { background: var(--accent-soft); }
|
|
253
|
+
.pe-slash-del {
|
|
254
|
+
margin-left: auto; padding: 2px 7px; border-radius: 6px; color: var(--faint);
|
|
255
|
+
font-size: 11px; opacity: 0; transition: opacity 0.1s ease;
|
|
256
|
+
}
|
|
257
|
+
.pe-slash-item:hover .pe-slash-del, .pe-slash-item.is-selected .pe-slash-del { opacity: 1; }
|
|
258
|
+
.pe-slash-del:hover { color: var(--red); background: var(--red-soft); }
|
|
249
259
|
.pe-slash-icon { width: 30px; height: 30px; flex: 0 0 auto; display: grid; place-items: center; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); color: var(--muted); }
|
|
250
260
|
.pe-slash-icon svg { width: 16px; height: 16px; }
|
|
251
261
|
.pe-slash-item.is-selected .pe-slash-icon { color: var(--accent-deep); }
|
|
@@ -482,14 +492,27 @@
|
|
|
482
492
|
/* floating link editor (⌘K) */
|
|
483
493
|
.pe-linkedit {
|
|
484
494
|
position: sticky; top: 12px; z-index: 60;
|
|
485
|
-
display: flex; gap: 8px; align-items: center;
|
|
486
495
|
background: var(--panel, #fff); border: 1px solid var(--border, #d5d2ca);
|
|
487
496
|
border-radius: 10px; padding: 8px 10px; margin: 0 auto 12px;
|
|
488
497
|
max-width: 560px; box-shadow: 0 10px 30px rgb(0 0 0 / 14%);
|
|
489
498
|
}
|
|
499
|
+
.pe-linkedit-row { display: flex; gap: 8px; align-items: center; }
|
|
490
500
|
.pe-linkedit input {
|
|
491
501
|
flex: 1; border: 0; outline: none; font-size: 14px; background: none; color: inherit;
|
|
492
502
|
}
|
|
503
|
+
.pe-link-results {
|
|
504
|
+
border-top: 1px solid var(--line, #e3e5ec); margin-top: 8px; padding-top: 6px;
|
|
505
|
+
display: flex; flex-direction: column;
|
|
506
|
+
}
|
|
507
|
+
.pe-link-results button {
|
|
508
|
+
display: flex; align-items: baseline; gap: 8px; border: 0; background: none;
|
|
509
|
+
text-align: left; padding: 7px 8px; border-radius: 8px; cursor: pointer;
|
|
510
|
+
font: inherit; color: var(--text, #1a1d25);
|
|
511
|
+
}
|
|
512
|
+
.pe-link-results button.is-selected { background: var(--panel-2, #f4f5f8); }
|
|
513
|
+
.pe-link-title { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
514
|
+
.pe-link-url { font-size: 12px; color: var(--faint, #8f95a3); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
515
|
+
.pe-link-kind { font-size: 10px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--faint, #8f95a3); }
|
|
493
516
|
|
|
494
517
|
/* shortcut cheat-sheet */
|
|
495
518
|
.pe-wordcount { display: flex; align-items: center; gap: 10px; }
|
|
@@ -594,3 +617,32 @@
|
|
|
594
617
|
.pe-infocard-icon svg { width: 14px; height: 14px; }
|
|
595
618
|
.pe-infocard-foot { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
|
596
619
|
.pe-infocard-href { flex: 1; }
|
|
620
|
+
|
|
621
|
+
/* toggle card (editor) — cards.css isn't loaded in the admin, so the box
|
|
622
|
+
look is restated here on the editor tokens */
|
|
623
|
+
.pe-toggle { border: 1px solid var(--line); border-radius: var(--r-md); padding: 12px 16px; margin: 1.2em 0; }
|
|
624
|
+
.pe-toggle.is-selected { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
|
|
625
|
+
.pe-toggle .pe-toggle-head { display: flex; align-items: center; gap: 8px; }
|
|
626
|
+
.pe-toggle-chevron {
|
|
627
|
+
border: 0; background: none; cursor: pointer; color: var(--muted, #666d7c);
|
|
628
|
+
font-size: 13px; padding: 2px 4px; transition: transform 0.15s ease;
|
|
629
|
+
}
|
|
630
|
+
.pe-toggle.is-open .pe-toggle-chevron { transform: rotate(90deg); }
|
|
631
|
+
.pe-toggle-summary { flex: 1; border: 0; outline: none; background: none; font: inherit; font-weight: 600; color: inherit; }
|
|
632
|
+
.pe-toggle .pc-toggle-body { margin-top: 10px; }
|
|
633
|
+
|
|
634
|
+
/* toc card (editor) */
|
|
635
|
+
.pe-toc .pc-toc {
|
|
636
|
+
display: flex; flex-direction: column; gap: 2px; margin: 0;
|
|
637
|
+
border-left: 3px solid var(--accent); background: var(--panel-2);
|
|
638
|
+
border-radius: 0 var(--r-md) var(--r-md) 0; padding: 14px 18px;
|
|
639
|
+
}
|
|
640
|
+
.pe-toc .pc-toc-link { padding: 2px 0; font-size: 14.5px; }
|
|
641
|
+
.pe-toc .pc-toc-link[data-depth='1'] { padding-left: 16px; }
|
|
642
|
+
.pe-toc .pc-toc-link[data-depth='2'] { padding-left: 32px; }
|
|
643
|
+
.pe-toc-title {
|
|
644
|
+
border: 0; outline: none; background: none; text-transform: uppercase;
|
|
645
|
+
letter-spacing: 0.08em; font-size: 11.5px; font-weight: 700;
|
|
646
|
+
color: var(--muted, #666d7c); margin-bottom: 6px; padding: 0;
|
|
647
|
+
}
|
|
648
|
+
.pe-toc-empty { color: var(--faint, #8f95a3); font-size: 13.5px; }
|
package/src/styles/site.css
CHANGED
|
@@ -77,6 +77,7 @@ pre .hljs-strong { font-weight: 700; }
|
|
|
77
77
|
code { font-family: ui-monospace, 'SF Mono', monospace; font-size: 0.9em; }
|
|
78
78
|
p code { background: var(--soft); padding: 2px 5px; border-radius: 4px; }
|
|
79
79
|
blockquote { margin: 16px 0; padding: 4px 18px; border-left: 3px solid var(--line); color: var(--muted); }
|
|
80
|
+
mark { background: #fde58a; border-radius: 3px; padding: 0 2px; }
|
|
80
81
|
table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 15px; line-height: 1.5; }
|
|
81
82
|
th, td { border: 1px solid var(--line); padding: 8px 12px; text-align: left; vertical-align: top; }
|
|
82
83
|
th { background: var(--soft); font-weight: 600; }
|
|
@@ -233,6 +234,8 @@ a.tag:hover { background: var(--accent); color: #fff; }
|
|
|
233
234
|
.post-kicker:empty { display: none; }
|
|
234
235
|
.post-layout { margin-top: 8px; }
|
|
235
236
|
.post-content > :first-child { margin-top: 0; }
|
|
237
|
+
/* in-body h1: below the post title's scale, above h2 */
|
|
238
|
+
.post-content h1 { font-size: 30px; margin: 1.2em 0 0.4em; }
|
|
236
239
|
|
|
237
240
|
.related-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
|
|
238
241
|
.related-card h3 { font-size: 18px; line-height: 1.3; margin: 4px 0 4px; }
|
|
@@ -425,3 +428,88 @@ a.tag:hover { background: var(--accent); color: #fff; }
|
|
|
425
428
|
.pc-cal-row .d { flex-basis: 84px; }
|
|
426
429
|
}
|
|
427
430
|
}
|
|
431
|
+
|
|
432
|
+
/* Site search (.pc-search) — the header trigger + overlay modal rendered by
|
|
433
|
+
Site.astro. The server renders the dialog around a real GET form to the
|
|
434
|
+
/search page, so search works with JS disabled; the inline script upgrades
|
|
435
|
+
it to live /api/search results. Scoped custom properties default to global
|
|
436
|
+
tokens — theme CSS and Settings → Code injection are unlayered, so either
|
|
437
|
+
overrides any of this without !important. */
|
|
438
|
+
@layer base {
|
|
439
|
+
.site-nav a.pc-search-trigger {
|
|
440
|
+
display: inline-flex; align-items: center; align-self: center;
|
|
441
|
+
color: var(--muted); padding: 2px;
|
|
442
|
+
}
|
|
443
|
+
.site-nav a.pc-search-trigger:hover { color: var(--ink); }
|
|
444
|
+
.pc-search-trigger svg { display: block; }
|
|
445
|
+
|
|
446
|
+
dialog.pc-search {
|
|
447
|
+
--search-bg: var(--panel);
|
|
448
|
+
--search-ink: var(--ink);
|
|
449
|
+
--search-muted: var(--muted);
|
|
450
|
+
--search-line: var(--line);
|
|
451
|
+
--search-accent: var(--accent);
|
|
452
|
+
--search-radius: 12px;
|
|
453
|
+
--search-veil: rgb(0 0 0 / 0.35);
|
|
454
|
+
--search-width: 640px;
|
|
455
|
+
--search-top: 9vh;
|
|
456
|
+
width: min(var(--search-width), 92vw);
|
|
457
|
+
margin: var(--search-top) auto auto;
|
|
458
|
+
border: 0; padding: 0; background: transparent; overflow: visible;
|
|
459
|
+
}
|
|
460
|
+
dialog.pc-search::backdrop { background: var(--search-veil); backdrop-filter: blur(3px); }
|
|
461
|
+
|
|
462
|
+
.pc-search-box {
|
|
463
|
+
display: flex; align-items: center; gap: 12px;
|
|
464
|
+
background: var(--search-bg); color: var(--search-ink);
|
|
465
|
+
border-radius: var(--search-radius);
|
|
466
|
+
box-shadow: 0 14px 40px rgb(0 0 0 / 0.18), 0 2px 6px rgb(0 0 0 / 0.08);
|
|
467
|
+
padding: 15px 18px;
|
|
468
|
+
}
|
|
469
|
+
.pc-search-box svg { flex: none; color: var(--search-muted); }
|
|
470
|
+
.pc-search-input {
|
|
471
|
+
flex: 1; min-width: 0; border: 0; outline: 0; background: transparent;
|
|
472
|
+
font: inherit; font-size: 17px; color: var(--search-ink);
|
|
473
|
+
}
|
|
474
|
+
.pc-search-input::placeholder { color: var(--search-muted); }
|
|
475
|
+
.pc-search-input::-webkit-search-cancel-button { display: none; }
|
|
476
|
+
.pc-search-kbd {
|
|
477
|
+
flex: none; border: 1px solid var(--search-line); border-radius: 6px;
|
|
478
|
+
background: transparent; color: var(--search-muted); cursor: pointer;
|
|
479
|
+
font-size: 11px; font-weight: 700; letter-spacing: 0.04em; padding: 2px 7px;
|
|
480
|
+
}
|
|
481
|
+
.pc-search-kbd:hover { color: var(--search-ink); }
|
|
482
|
+
|
|
483
|
+
.pc-search-results {
|
|
484
|
+
margin-top: 10px; padding: 2px 0 8px;
|
|
485
|
+
background: var(--search-bg); border-radius: var(--search-radius);
|
|
486
|
+
box-shadow: 0 14px 40px rgb(0 0 0 / 0.16);
|
|
487
|
+
max-height: min(56vh, 460px); overflow-y: auto;
|
|
488
|
+
}
|
|
489
|
+
.pc-search-group {
|
|
490
|
+
font-size: 10.5px; font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase;
|
|
491
|
+
color: var(--search-muted); padding: 14px 18px 4px;
|
|
492
|
+
}
|
|
493
|
+
a.pc-search-hit {
|
|
494
|
+
display: flex; align-items: baseline; gap: 10px;
|
|
495
|
+
padding: 8px 18px; text-decoration: none; color: var(--search-ink); font-size: 15.5px;
|
|
496
|
+
}
|
|
497
|
+
a.pc-search-hit:hover, a.pc-search-hit.sel {
|
|
498
|
+
background: color-mix(in srgb, var(--search-ink) 6%, transparent);
|
|
499
|
+
}
|
|
500
|
+
.pc-search-t { min-width: 0; }
|
|
501
|
+
.pc-search-snip {
|
|
502
|
+
display: block; font-size: 13px; color: var(--search-muted);
|
|
503
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
504
|
+
}
|
|
505
|
+
.pc-search-snip mark {
|
|
506
|
+
background: color-mix(in srgb, var(--search-accent) 22%, transparent);
|
|
507
|
+
color: inherit; border-radius: 3px; padding: 0 1px;
|
|
508
|
+
}
|
|
509
|
+
.pc-search-m { margin-left: auto; flex: none; font-size: 12.5px; color: var(--search-muted); }
|
|
510
|
+
.pc-search-empty { padding: 18px; color: var(--search-muted); font-size: 14.5px; }
|
|
511
|
+
|
|
512
|
+
@media (max-width: 640px) {
|
|
513
|
+
dialog.pc-search { --search-top: 4vh; width: 94vw; }
|
|
514
|
+
}
|
|
515
|
+
}
|
package/src/lib/repo/search.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Post } from '../schema';
|
|
2
|
-
|
|
3
|
-
// Shared ranking for the LIKE-based search: title hits beat excerpt hits beat
|
|
4
|
-
// body hits; recency breaks ties. Swap the adapters' SQL for FTS later and
|
|
5
|
-
// this ranking can go — the repo method signature stays the same.
|
|
6
|
-
export function rankByQuery(posts: Post[], query: string): Post[] {
|
|
7
|
-
const q = query.toLowerCase();
|
|
8
|
-
const score = (p: Post) =>
|
|
9
|
-
(p.title.toLowerCase().includes(q) ? 4 : 0) +
|
|
10
|
-
(p.excerpt.toLowerCase().includes(q) ? 2 : 0) +
|
|
11
|
-
(p.tags.some((t) => t.toLowerCase().includes(q)) ? 2 : 0) +
|
|
12
|
-
1;
|
|
13
|
-
return [...posts].sort((a, b) => score(b) - score(a) || (b.publishedAt ?? 0) - (a.publishedAt ?? 0));
|
|
14
|
-
}
|