@acms-subir/core 0.0.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/cli.mjs +75 -0
- package/integration.mjs +286 -0
- package/package.json +89 -0
- package/src/actions/index.ts +502 -0
- package/src/components/DesignManager.tsx +542 -0
- package/src/components/LinksManager.tsx +694 -0
- package/src/components/Modal.astro +65 -0
- package/src/components/NavManager.tsx +168 -0
- package/src/components/WidgetRenderer.astro +84 -0
- package/src/components/builder/PageBuilder.tsx +575 -0
- package/src/components/editor/ImagePicker.tsx +215 -0
- package/src/components/editor/PostEditor.tsx +1011 -0
- package/src/components/editor/PreviewOverlay.tsx +77 -0
- package/src/components/editor/extensions/calendar.tsx +230 -0
- package/src/components/editor/extensions/cards.tsx +488 -0
- package/src/components/editor/extensions/codeBlock.tsx +70 -0
- package/src/components/editor/extensions/infoCards.tsx +270 -0
- package/src/components/editor/extensions/mediaCards.tsx +376 -0
- package/src/components/editor/extensions/postList.tsx +130 -0
- package/src/components/editor/extensions/slash.tsx +250 -0
- package/src/components/editor/extensions/tableTyping.ts +63 -0
- package/src/components/editor/imaging.ts +146 -0
- package/src/components/media/CropModal.tsx +229 -0
- package/src/components/media/MediaLibrary.tsx +295 -0
- package/src/layouts/Admin.astro +119 -0
- package/src/layouts/Site.astro +218 -0
- package/src/lib/auth.ts +94 -0
- package/src/lib/backup/unzip.ts +38 -0
- package/src/lib/backup/zip.ts +105 -0
- package/src/lib/blocksToHtml.ts +68 -0
- package/src/lib/brand.ts +26 -0
- package/src/lib/calendar/ics.ts +402 -0
- package/src/lib/calendar/render.ts +275 -0
- package/src/lib/calendar/src.ts +68 -0
- package/src/lib/content.ts +53 -0
- package/src/lib/dates.ts +34 -0
- package/src/lib/dynamicBlocks.ts +115 -0
- package/src/lib/env.ts +6 -0
- package/src/lib/highlightCode.ts +44 -0
- package/src/lib/i18n.ts +40 -0
- package/src/lib/markdown.ts +7 -0
- package/src/lib/media/store.ts +95 -0
- package/src/lib/net.ts +51 -0
- package/src/lib/preview.ts +28 -0
- package/src/lib/repo/d1.ts +44 -0
- package/src/lib/repo/index.ts +23 -0
- package/src/lib/repo/migrations.ts +231 -0
- package/src/lib/repo/search.ts +14 -0
- package/src/lib/repo/sql.ts +721 -0
- package/src/lib/repo/sqlite.ts +43 -0
- package/src/lib/repo/types.ts +132 -0
- package/src/lib/schema.ts +286 -0
- package/src/lib/seo.ts +37 -0
- package/src/lib/theme.ts +206 -0
- package/src/lib/widgets.ts +108 -0
- package/src/middleware.ts +109 -0
- package/src/pages/[...slug].astro +43 -0
- package/src/pages/admin/c/[collection]/[id].astro +56 -0
- package/src/pages/admin/c/[collection]/index.astro +163 -0
- package/src/pages/admin/design.astro +42 -0
- package/src/pages/admin/index.astro +145 -0
- package/src/pages/admin/links.astro +21 -0
- package/src/pages/admin/login.astro +83 -0
- package/src/pages/admin/logout.ts +7 -0
- package/src/pages/admin/media.astro +13 -0
- package/src/pages/admin/navigation.astro +17 -0
- package/src/pages/admin/pages/[id].astro +49 -0
- package/src/pages/admin/pages/index.astro +100 -0
- package/src/pages/admin/posts/[id].astro +6 -0
- package/src/pages/admin/posts/index.astro +7 -0
- package/src/pages/admin/settings.astro +512 -0
- package/src/pages/admin/tags.astro +101 -0
- package/src/pages/admin/users.astro +149 -0
- package/src/pages/api/backup.ts +63 -0
- package/src/pages/api/drafts.ts +106 -0
- package/src/pages/api/media/upload.ts +89 -0
- package/src/pages/api/restore.ts +102 -0
- package/src/pages/author/[slug].astro +66 -0
- package/src/pages/blog/[slug].astro +240 -0
- package/src/pages/blog/index.astro +51 -0
- package/src/pages/blog/tag/[slug].astro +83 -0
- package/src/pages/embed/[pageId]/[blockId].ts +45 -0
- package/src/pages/go/[id].ts +10 -0
- package/src/pages/index.astro +52 -0
- package/src/pages/links.astro +110 -0
- package/src/pages/llms.txt.ts +47 -0
- package/src/pages/media/[...key].ts +18 -0
- package/src/pages/preview/[type]/[id].astro +52 -0
- package/src/pages/robots.txt.ts +28 -0
- package/src/pages/rss.xml.ts +58 -0
- package/src/pages/search.astro +83 -0
- package/src/pages/sitemap.xml.ts +55 -0
- package/src/styles/admin.css +652 -0
- package/src/styles/builder.css +121 -0
- package/src/styles/cards.css +176 -0
- package/src/styles/editor.css +596 -0
- package/src/styles/site.css +427 -0
- package/src/themes/editorial/templates/home.astro +127 -0
- package/src/themes/editorial/templates/post-minimal.astro +50 -0
- package/src/themes/editorial/theme.css +108 -0
- package/src/themes/editorial/theme.json +20 -0
- package/src/themes/field/theme.json +17 -0
- package/src/themes/ink/theme.json +17 -0
- package/src/themes/night/theme.css +7 -0
- package/src/themes/night/theme.json +17 -0
- package/src/virtual.d.ts +25 -0
- package/src/worker.ts +28 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/* Wrapped in @layer base so theme CSS (inlined unlayered by Site.astro)
|
|
2
|
+
always wins ties, regardless of bundle order in the built head. */
|
|
3
|
+
@layer base {
|
|
4
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
5
|
+
/* Base values only — the active theme (src/themes/) overrides these via an
|
|
6
|
+
inlined :root block emitted by Site.astro, plus its own theme.css. */
|
|
7
|
+
:root {
|
|
8
|
+
--bg: #faf9f7;
|
|
9
|
+
--ink: #1e1f24;
|
|
10
|
+
--muted: #6b6e78;
|
|
11
|
+
--line: #e5e3de;
|
|
12
|
+
--accent: #4f46e5;
|
|
13
|
+
--panel: #ffffff;
|
|
14
|
+
--soft: #eceae5;
|
|
15
|
+
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
|
|
16
|
+
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
|
|
17
|
+
--content-width: 760px;
|
|
18
|
+
}
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
background: var(--bg);
|
|
22
|
+
color: var(--ink);
|
|
23
|
+
font-family: var(--font-body);
|
|
24
|
+
font-size: 17px;
|
|
25
|
+
line-height: 1.65;
|
|
26
|
+
-webkit-font-smoothing: antialiased;
|
|
27
|
+
}
|
|
28
|
+
.site-header {
|
|
29
|
+
max-width: var(--content-width);
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
padding: 28px 20px 0;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: baseline;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
gap: 16px;
|
|
36
|
+
flex-wrap: wrap;
|
|
37
|
+
}
|
|
38
|
+
.site-header .brand { font-weight: 700; font-size: 18px; text-decoration: none; color: var(--ink); display: flex; align-items: center; }
|
|
39
|
+
.site-header .brand img.brand-logo { height: 30px; width: auto; display: block; }
|
|
40
|
+
.site-nav { display: flex; gap: 18px; flex-wrap: wrap; align-items: center; }
|
|
41
|
+
.site-nav a { color: var(--muted); text-decoration: none; font-size: 15px; }
|
|
42
|
+
.site-nav a:hover, .site-nav a[aria-current='page'] { color: var(--ink); }
|
|
43
|
+
|
|
44
|
+
/* language switcher — rendered when acms({ i18n }) lists >1 locale */
|
|
45
|
+
.lang-switch { display: inline-flex; border: 1px solid var(--ink); border-radius: 999px; overflow: hidden; }
|
|
46
|
+
.lang-switch a { padding: 2px 11px; font-size: 12.5px; font-weight: 700; color: var(--ink); }
|
|
47
|
+
.lang-switch a:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 8%, transparent); }
|
|
48
|
+
.lang-switch a.on, .lang-switch a.on:hover { background: var(--ink); color: var(--bg, #fff); }
|
|
49
|
+
main.site-main { max-width: var(--content-width); margin: 0 auto; padding: 24px 20px 80px; }
|
|
50
|
+
.site-footer { max-width: var(--content-width); margin: 0 auto; padding: 24px 20px 48px; color: var(--muted); font-size: 13.5px; border-top: 1px solid var(--line); }
|
|
51
|
+
.site-footer p { margin: 0 0 6px; }
|
|
52
|
+
.site-footer p:last-child { margin-bottom: 0; }
|
|
53
|
+
.site-footer a { color: var(--muted); text-decoration-color: var(--line); }
|
|
54
|
+
.site-footer a:hover { color: var(--ink); }
|
|
55
|
+
a { color: var(--accent); }
|
|
56
|
+
img { max-width: 100%; height: auto; }
|
|
57
|
+
h1, h2, h3, h4 { font-family: var(--font-heading); line-height: 1.25; letter-spacing: -0.01em; }
|
|
58
|
+
pre { background: #23242a; color: #e8e9ed; border-radius: 10px; padding: 14px 16px; overflow-x: auto; font-size: 14px; position: relative; }
|
|
59
|
+
/* language badge + dark token palette, emitted by src/lib/highlightCode.ts */
|
|
60
|
+
pre[data-language]::before {
|
|
61
|
+
content: attr(data-language); position: absolute; top: 8px; right: 12px;
|
|
62
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: #8a90a3;
|
|
63
|
+
font-family: var(--font-body, inherit);
|
|
64
|
+
}
|
|
65
|
+
pre .hljs-comment, pre .hljs-quote { color: #8b949e; font-style: italic; }
|
|
66
|
+
pre .hljs-keyword, pre .hljs-selector-tag, pre .hljs-doctag, pre .hljs-template-tag, pre .hljs-operator { color: #ff7b72; }
|
|
67
|
+
pre .hljs-string, pre .hljs-regexp, pre .hljs-addition { color: #a5d6ff; }
|
|
68
|
+
pre .hljs-title, pre .hljs-section { color: #d2a8ff; }
|
|
69
|
+
pre .hljs-number, pre .hljs-literal, pre .hljs-attr, pre .hljs-variable, pre .hljs-template-variable,
|
|
70
|
+
pre .hljs-type, pre .hljs-selector-class, pre .hljs-selector-id, pre .hljs-meta,
|
|
71
|
+
pre .hljs-attribute, pre .hljs-property { color: #79c0ff; }
|
|
72
|
+
pre .hljs-name, pre .hljs-tag, pre .hljs-bullet { color: #7ee787; }
|
|
73
|
+
pre .hljs-built_in, pre .hljs-symbol { color: #ffa657; }
|
|
74
|
+
pre .hljs-deletion { color: #ffa198; }
|
|
75
|
+
pre .hljs-emphasis { font-style: italic; }
|
|
76
|
+
pre .hljs-strong { font-weight: 700; }
|
|
77
|
+
code { font-family: ui-monospace, 'SF Mono', monospace; font-size: 0.9em; }
|
|
78
|
+
p code { background: var(--soft); padding: 2px 5px; border-radius: 4px; }
|
|
79
|
+
blockquote { margin: 16px 0; padding: 4px 18px; border-left: 3px solid var(--line); color: var(--muted); }
|
|
80
|
+
table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 15px; line-height: 1.5; }
|
|
81
|
+
th, td { border: 1px solid var(--line); padding: 8px 12px; text-align: left; vertical-align: top; }
|
|
82
|
+
th { background: var(--soft); font-weight: 600; }
|
|
83
|
+
th p, td p { margin: 0; }
|
|
84
|
+
/* saved empty cells serialize as <p></p> — keep them one line tall */
|
|
85
|
+
th p:empty::before, td p:empty::before { content: '\00a0'; }
|
|
86
|
+
|
|
87
|
+
/* widgets */
|
|
88
|
+
.w-hero { padding: 56px 0 40px; }
|
|
89
|
+
.w-hero.center { text-align: center; }
|
|
90
|
+
.w-hero h1 { font-size: 40px; margin: 0 0 10px; }
|
|
91
|
+
.w-hero .sub { font-size: 19px; color: var(--muted); margin: 0 auto 22px; max-width: 55ch; }
|
|
92
|
+
.w-hero.center .sub { margin-left: auto; margin-right: auto; }
|
|
93
|
+
.w-btn { display: inline-block; padding: 10px 22px; border-radius: 10px; text-decoration: none; font-weight: 600; font-size: 15px; }
|
|
94
|
+
.w-btn.primary { background: var(--accent); color: #fff; }
|
|
95
|
+
.w-btn.outline { border: 1.5px solid var(--accent); color: var(--accent); }
|
|
96
|
+
.w-image figcaption { color: var(--muted); font-size: 14px; margin-top: 6px; }
|
|
97
|
+
.w-spacer.s { height: 16px; } .w-spacer.m { height: 40px; } .w-spacer.l { height: 80px; }
|
|
98
|
+
.w-embed iframe, .w-custom iframe { width: 100%; border: 0; border-radius: 10px; display: block; }
|
|
99
|
+
|
|
100
|
+
/* blog */
|
|
101
|
+
.post-list { list-style: none; padding: 0; margin: 0; }
|
|
102
|
+
.post-list li { padding: 18px 0; border-bottom: 1px solid var(--line); }
|
|
103
|
+
.post-list a { font-size: 20px; font-weight: 650; text-decoration: none; color: var(--ink); }
|
|
104
|
+
.post-list a:hover { color: var(--accent); }
|
|
105
|
+
.post-meta { color: var(--muted); font-size: 14px; margin-top: 2px; }
|
|
106
|
+
.tag { display: inline-block; background: var(--soft); color: var(--muted); font-size: 12.5px; padding: 2px 9px; border-radius: 99px; margin-right: 6px; text-decoration: none; }
|
|
107
|
+
a.tag:hover { background: var(--accent); color: #fff; }
|
|
108
|
+
.byline { color: var(--muted); }
|
|
109
|
+
.tag-kicker { text-transform: uppercase; letter-spacing: 0.09em; font-size: 12px; color: var(--accent); margin: 24px 0 0; font-weight: 600; }
|
|
110
|
+
.tag-kicker + h1 { margin-top: 4px; }
|
|
111
|
+
.tag-desc { color: var(--muted); font-size: 17px; margin-top: -8px; }
|
|
112
|
+
.post-cover {
|
|
113
|
+
display: block; width: 100%; aspect-ratio: 2 / 1; object-fit: cover;
|
|
114
|
+
border-radius: 12px; margin: 12px 0 28px; background: var(--soft);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* links page */
|
|
118
|
+
.links-page { max-width: 520px; margin: 0 auto; text-align: center; padding-top: 32px; }
|
|
119
|
+
.links-page h1 { margin-bottom: 4px; }
|
|
120
|
+
.links-avatar {
|
|
121
|
+
width: 96px; height: 96px; border-radius: 50%; object-fit: cover;
|
|
122
|
+
margin: 0 auto 14px; display: block; box-shadow: 0 4px 16px rgb(30 31 36 / 12%);
|
|
123
|
+
}
|
|
124
|
+
.links-page .tagline { color: var(--muted); margin: 0 0 32px; white-space: pre-wrap; }
|
|
125
|
+
.link-btn {
|
|
126
|
+
display: grid; grid-template-columns: 28px 1fr 28px; align-items: center; gap: 8px;
|
|
127
|
+
margin: 14px 0; padding: 15px 18px;
|
|
128
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
|
129
|
+
color: var(--ink); font-weight: 600; text-decoration: none; font-size: 16px;
|
|
130
|
+
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
131
|
+
}
|
|
132
|
+
.link-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgb(30 31 36 / 8%); }
|
|
133
|
+
.link-btn .link-label { grid-column: 2; }
|
|
134
|
+
.link-ico { grid-column: 1; display: grid; place-items: center; width: 28px; height: 28px; }
|
|
135
|
+
.link-ico svg { width: 22px; height: 22px; fill: currentColor; }
|
|
136
|
+
.link-ico img { width: 24px; height: 24px; border-radius: 6px; object-fit: cover; display: block; }
|
|
137
|
+
.link-ico-emoji { font-size: 20px; line-height: 1; }
|
|
138
|
+
.link-heading { font-size: 20px; margin: 26px 0 2px; letter-spacing: -0.01em; }
|
|
139
|
+
.link-text { color: var(--muted); font-size: 15px; margin: 10px 0; white-space: pre-wrap; }
|
|
140
|
+
.link-spacer { width: 100%; }
|
|
141
|
+
.link-html { margin: 14px 0; text-align: initial; }
|
|
142
|
+
.link-html > :first-child { margin-top: 0; }
|
|
143
|
+
.link-html > :last-child { margin-bottom: 0; }
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@layer base {
|
|
148
|
+
/* managed footer navigation areas */
|
|
149
|
+
.footer-nav, .social-nav { display: flex; flex-wrap: wrap; gap: 14px 18px; margin-bottom: 12px; }
|
|
150
|
+
.footer-nav a, .social-nav a { color: var(--muted); text-decoration: none; font-size: 13.5px; }
|
|
151
|
+
.footer-nav a:hover, .social-nav a:hover { color: var(--ink); }
|
|
152
|
+
.social-nav a { text-transform: uppercase; letter-spacing: 0.06em; font-size: 12px; }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@layer base {
|
|
156
|
+
/* pagination + tag landing pages */
|
|
157
|
+
.pager { display: flex; align-items: center; justify-content: space-between; margin-top: 32px; font-size: 14.5px; }
|
|
158
|
+
.pager a { text-decoration: none; }
|
|
159
|
+
.pager-status { color: var(--muted); font-size: 13px; }
|
|
160
|
+
.tag-hero { margin-bottom: 8px; }
|
|
161
|
+
.post-item { display: flex; gap: 18px; align-items: flex-start; }
|
|
162
|
+
.post-thumb { flex: 0 0 150px; }
|
|
163
|
+
.post-thumb img { width: 150px; aspect-ratio: 1.6 / 1; object-fit: cover; border-radius: 8px; background: var(--soft); display: block; }
|
|
164
|
+
@media (max-width: 560px) { .post-item { flex-direction: column; } .post-thumb, .post-thumb img { width: 100%; flex-basis: auto; } }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@layer base {
|
|
168
|
+
/* dynamic post-list blocks (expanded at request time) */
|
|
169
|
+
.dyn-postlist { display: grid; gap: 22px; margin: 28px 0; }
|
|
170
|
+
.dyn-postlist.is-empty p { color: var(--muted); border: 1.5px dashed var(--line); border-radius: 8px; padding: 22px; text-align: center; font-size: 14px; margin: 0; }
|
|
171
|
+
.dyn-postlist .dp-meta { color: var(--muted); font-size: 12.5px; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
172
|
+
.dyn-postlist .dp-title { margin: 4px 0 4px; font-size: 21px; line-height: 1.25; }
|
|
173
|
+
.dyn-postlist .dp-title a { color: var(--ink); text-decoration: none; }
|
|
174
|
+
.dyn-postlist .dp-title a:hover { color: var(--accent); }
|
|
175
|
+
.dyn-postlist .dp-excerpt { color: var(--muted); font-size: 15px; margin: 2px 0 0; }
|
|
176
|
+
.dyn-postlist .dp-thumb img { width: 100%; border-radius: 8px; object-fit: cover; background: var(--soft); display: block; }
|
|
177
|
+
|
|
178
|
+
.dyn-postlist[data-layout='list'] .dp-item { display: flex; gap: 18px; align-items: flex-start; padding-bottom: 20px; border-bottom: 1px solid var(--line); }
|
|
179
|
+
.dyn-postlist[data-layout='list'] .dp-item:last-child { border-bottom: none; padding-bottom: 0; }
|
|
180
|
+
.dyn-postlist[data-layout='list'] .dp-thumb { flex: 0 0 150px; }
|
|
181
|
+
.dyn-postlist[data-layout='list'] .dp-thumb img { aspect-ratio: 1.6 / 1; }
|
|
182
|
+
|
|
183
|
+
.dyn-postlist[data-layout='grid'] { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
|
184
|
+
.dyn-postlist[data-layout='grid'] .dp-thumb img { aspect-ratio: 1.9 / 1; margin-bottom: 8px; }
|
|
185
|
+
.dyn-postlist[data-layout='grid'] .dp-title { font-size: 19px; }
|
|
186
|
+
|
|
187
|
+
.dyn-postlist[data-layout='feature'] .dp-thumb img { aspect-ratio: 2 / 1; margin-bottom: 10px; }
|
|
188
|
+
.dyn-postlist[data-layout='feature'] .dp-title { font-size: 28px; }
|
|
189
|
+
|
|
190
|
+
@media (max-width: 560px) {
|
|
191
|
+
.dyn-postlist[data-layout='list'] .dp-item { flex-direction: column; }
|
|
192
|
+
.dyn-postlist[data-layout='list'] .dp-thumb { flex-basis: auto; width: 100%; }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@layer base {
|
|
197
|
+
/* author profiles + post page extras */
|
|
198
|
+
.author-hero { display: flex; align-items: center; gap: 20px; margin: 8px 0 20px; }
|
|
199
|
+
.author-avatar { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: var(--soft); }
|
|
200
|
+
.author-initial { display: inline-flex; align-items: center; justify-content: center; font-family: var(--font-heading); font-size: 28px; color: var(--muted); }
|
|
201
|
+
.author-hero h1 { margin: 2px 0 4px; }
|
|
202
|
+
|
|
203
|
+
.post-share { display: flex; align-items: center; gap: 10px; margin: 36px 0 0; }
|
|
204
|
+
.post-share-label { text-transform: uppercase; letter-spacing: 0.08em; font-size: 11.5px; color: var(--muted); margin-right: 2px; }
|
|
205
|
+
.post-share a, .post-share-copy {
|
|
206
|
+
display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
|
|
207
|
+
border-radius: 50%; border: 1px solid var(--line); color: var(--muted); background: none; cursor: pointer;
|
|
208
|
+
}
|
|
209
|
+
.post-share a:hover, .post-share-copy:hover { color: var(--ink); border-color: var(--muted); }
|
|
210
|
+
.post-share-copy.is-copied { color: var(--accent); border-color: var(--accent); }
|
|
211
|
+
|
|
212
|
+
.author-card {
|
|
213
|
+
display: flex; gap: 18px; align-items: flex-start;
|
|
214
|
+
border-top: 1px solid var(--line); margin-top: 28px; padding-top: 24px;
|
|
215
|
+
}
|
|
216
|
+
.author-card .author-avatar { width: 64px; height: 64px; }
|
|
217
|
+
.author-card-name { font-size: 22px; margin: 4px 0 4px; }
|
|
218
|
+
.author-card-name a { color: var(--ink); text-decoration: none; }
|
|
219
|
+
.author-card-name a:hover { color: var(--accent); }
|
|
220
|
+
.author-card-bio { color: var(--muted); font-size: 15px; margin: 0 0 8px; }
|
|
221
|
+
.author-card-more { text-transform: uppercase; letter-spacing: 0.08em; font-size: 11.5px; text-decoration: none; }
|
|
222
|
+
|
|
223
|
+
.related { border-top: 1px solid var(--line); margin-top: 32px; padding-top: 8px; }
|
|
224
|
+
.related-title { font-size: 24px; }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@layer base {
|
|
228
|
+
/* post page structure: header / cover / body+share / author / related.
|
|
229
|
+
Base = single column with share row after the body; themes can turn
|
|
230
|
+
.post-layout into a grid with a share rail. */
|
|
231
|
+
.post-header { margin-bottom: 4px; }
|
|
232
|
+
.post-kicker { margin: 24px 0 6px; }
|
|
233
|
+
.post-kicker:empty { display: none; }
|
|
234
|
+
.post-layout { margin-top: 8px; }
|
|
235
|
+
.post-content > :first-child { margin-top: 0; }
|
|
236
|
+
|
|
237
|
+
.related-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
|
|
238
|
+
.related-card h3 { font-size: 18px; line-height: 1.3; margin: 4px 0 4px; }
|
|
239
|
+
.related-card h3 a { color: var(--ink); text-decoration: none; }
|
|
240
|
+
.related-card h3 a:hover { color: var(--accent); }
|
|
241
|
+
.related-thumb img { width: 100%; aspect-ratio: 1.7 / 1; object-fit: cover; border-radius: 8px; background: var(--soft); display: block; margin-bottom: 8px; }
|
|
242
|
+
.related-excerpt { color: var(--muted); font-size: 13.5px; margin: 2px 0 0; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@layer base {
|
|
246
|
+
/* feature-image styles, driven by theme post.cover / Design overrides */
|
|
247
|
+
.post-cover.is-full {
|
|
248
|
+
width: 100vw; max-width: none; margin-left: calc(50% - 50vw); margin-right: 0;
|
|
249
|
+
border-radius: 0; aspect-ratio: auto; max-height: 62vh;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@layer base {
|
|
254
|
+
/* overlay feature image: full-bleed hero, bottom gradient scrim for
|
|
255
|
+
legibility (fades out ~60% up so most of the image stays clean),
|
|
256
|
+
title/meta stacked on top */
|
|
257
|
+
.post-hero {
|
|
258
|
+
position: relative; width: 100vw; margin-left: calc(50% - 50vw);
|
|
259
|
+
min-height: clamp(340px, 62vh, 660px);
|
|
260
|
+
display: flex; align-items: flex-end; justify-content: center;
|
|
261
|
+
margin-bottom: 40px;
|
|
262
|
+
}
|
|
263
|
+
.post-hero-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
264
|
+
.post-hero-scrim {
|
|
265
|
+
position: absolute; inset: 0;
|
|
266
|
+
background: linear-gradient(to top, rgb(0 0 0 / 74%) 0%, rgb(0 0 0 / 34%) 32%, transparent 60%);
|
|
267
|
+
}
|
|
268
|
+
.post-hero-content {
|
|
269
|
+
position: relative; width: 100%; max-width: var(--content-width);
|
|
270
|
+
padding: 0 20px 44px; text-align: center; color: #fff;
|
|
271
|
+
}
|
|
272
|
+
.post-hero-content h1 { color: #fff; margin: 6px 0 10px; text-shadow: 0 1px 16px rgb(0 0 0 / 40%); }
|
|
273
|
+
.post-hero-content .post-meta { color: rgb(255 255 255 / 85%); }
|
|
274
|
+
.post-hero-content .byline a { color: rgb(255 255 255 / 85%); }
|
|
275
|
+
.post-hero-content .byline a:hover { color: #fff; }
|
|
276
|
+
.post-hero-content .tag { background: rgb(255 255 255 / 16%); color: rgb(255 255 255 / 90%); }
|
|
277
|
+
.post-hero-content a.tag:hover { background: rgb(255 255 255 / 30%); color: #fff; }
|
|
278
|
+
@media (max-width: 640px) { .post-hero { min-height: 300px; } .post-hero-content { padding-bottom: 30px; } }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* announcement bar */
|
|
282
|
+
.announcement-bar {
|
|
283
|
+
display: flex; align-items: center; justify-content: center; gap: 10px;
|
|
284
|
+
background: var(--ink); color: var(--bg);
|
|
285
|
+
font-size: 14px; padding: 9px 40px; text-align: center; position: relative;
|
|
286
|
+
}
|
|
287
|
+
.announcement-bar a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
|
|
288
|
+
.announcement-x {
|
|
289
|
+
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
|
|
290
|
+
background: none; border: 0; color: inherit; opacity: 0.7; cursor: pointer;
|
|
291
|
+
font-size: 12px; padding: 6px;
|
|
292
|
+
}
|
|
293
|
+
.announcement-x:hover { opacity: 1; }
|
|
294
|
+
|
|
295
|
+
/* Calendar card (dynamic block): month / week / upcoming views rendered by
|
|
296
|
+
src/lib/calendar/render.ts. Everything is driven by the scoped --cal-*
|
|
297
|
+
custom properties (defaulting to theme tokens) and stable .pc-cal-* class
|
|
298
|
+
hooks — the widget's public styling API. Theme CSS and Settings → Code
|
|
299
|
+
injection are unlayered, so either can override any of this without
|
|
300
|
+
!important. */
|
|
301
|
+
@layer base {
|
|
302
|
+
.pc-calendar {
|
|
303
|
+
--cal-bg: var(--panel);
|
|
304
|
+
--cal-ink: var(--ink);
|
|
305
|
+
--cal-muted: var(--muted);
|
|
306
|
+
--cal-line: var(--line);
|
|
307
|
+
--cal-accent: var(--accent);
|
|
308
|
+
--cal-soft: var(--soft);
|
|
309
|
+
--cal-radius: 12px;
|
|
310
|
+
--cal-dot-2: #2f9e6e; --cal-dot-3: #b3541e; --cal-dot-4: #8250df; --cal-dot-5: #c2255c; --cal-dot-6: #0c8599;
|
|
311
|
+
margin: 28px 0;
|
|
312
|
+
}
|
|
313
|
+
.pc-cal-heading { font-family: var(--font-heading); font-size: 24px; font-weight: 800; letter-spacing: -0.01em; color: var(--cal-ink); margin: 0 0 3px; }
|
|
314
|
+
.pc-cal-sub { color: var(--cal-muted); font-size: 14px; margin: 0 0 16px; }
|
|
315
|
+
.pc-cal-frame { position: relative; background: var(--cal-bg); border: 1px solid var(--cal-line); border-radius: var(--cal-radius); }
|
|
316
|
+
|
|
317
|
+
.pc-cal-toolbar { display: flex; align-items: center; gap: 8px; padding: 13px 16px; border-bottom: 1px solid var(--cal-line); }
|
|
318
|
+
.pc-cal-title { font-weight: 700; font-size: 15.5px; color: var(--cal-ink); margin-right: auto; }
|
|
319
|
+
.pc-cal-btn {
|
|
320
|
+
font: 600 12.5px/1 var(--font-body); color: var(--cal-ink); background: transparent;
|
|
321
|
+
border: 1px solid var(--cal-line); border-radius: 8px; padding: 7px 11px; cursor: pointer;
|
|
322
|
+
}
|
|
323
|
+
.pc-cal-btn:hover { border-color: var(--cal-accent); color: var(--cal-accent); }
|
|
324
|
+
|
|
325
|
+
/* month view — minmax(0, 1fr) so long event names can never stretch a
|
|
326
|
+
column: all seven stay exactly equal and chips truncate instead */
|
|
327
|
+
.pc-cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
|
|
328
|
+
.pc-cal-dow {
|
|
329
|
+
font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
|
|
330
|
+
color: var(--cal-muted); text-align: center; padding: 10px 0 8px; border-bottom: 1px solid var(--cal-line);
|
|
331
|
+
}
|
|
332
|
+
.pc-cal-day { min-width: 0; min-height: 76px; border-bottom: 1px solid var(--cal-line); border-right: 1px solid var(--cal-line); padding: 5px 4px; font-size: 12px; }
|
|
333
|
+
.pc-cal-day:nth-child(7n + 7) { border-right: none; }
|
|
334
|
+
.pc-cal-grid .pc-cal-day:nth-last-child(-n + 7) { border-bottom: none; }
|
|
335
|
+
.pc-cal-date {
|
|
336
|
+
display: flex; width: fit-content; margin: 0 auto; align-items: center; justify-content: center;
|
|
337
|
+
min-width: 22px; height: 22px; border-radius: 999px; font-weight: 600; color: var(--cal-ink); padding: 0 4px;
|
|
338
|
+
}
|
|
339
|
+
.pc-cal-day.is-out .pc-cal-date { color: var(--cal-muted); opacity: 0.55; }
|
|
340
|
+
.pc-cal-day.is-today .pc-cal-date, .pc-cal-wday.is-today .pc-cal-date { background: var(--cal-accent); color: var(--cal-bg); }
|
|
341
|
+
|
|
342
|
+
/* event chips (buttons: they open the details popover). One line, title
|
|
343
|
+
truncates with an ellipsis — chips never wrap or widen their column. The
|
|
344
|
+
color dot only appears on multi-calendar cards (.is-multi), where it
|
|
345
|
+
carries meaning; on a single calendar it's just noise. */
|
|
346
|
+
.pc-cal-event {
|
|
347
|
+
display: flex; width: 100%; min-width: 0; align-items: center; gap: 5px; margin-top: 3px; padding: 2px 5px;
|
|
348
|
+
border: 0; border-radius: 6px; background: none; color: var(--cal-ink);
|
|
349
|
+
font-family: var(--font-body); font-size: 11px; line-height: 1.5; text-align: left; cursor: pointer;
|
|
350
|
+
}
|
|
351
|
+
.pc-cal-event:hover { background: var(--cal-soft); }
|
|
352
|
+
.pc-cal-event::before { content: ''; display: none; flex: 0 0 6px; width: 6px; height: 6px; border-radius: 99px; background: var(--cal-dot, var(--cal-accent)); }
|
|
353
|
+
.pc-calendar.is-multi .pc-cal-event::before { display: block; }
|
|
354
|
+
[data-cal='1'] { --cal-dot: var(--cal-dot-2); }
|
|
355
|
+
[data-cal='2'] { --cal-dot: var(--cal-dot-3); }
|
|
356
|
+
[data-cal='3'] { --cal-dot: var(--cal-dot-4); }
|
|
357
|
+
[data-cal='4'] { --cal-dot: var(--cal-dot-5); }
|
|
358
|
+
[data-cal='5'] { --cal-dot: var(--cal-dot-6); }
|
|
359
|
+
.pc-cal-event b { font-weight: 500; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
360
|
+
.pc-cal-event .t { color: var(--cal-muted); flex: none; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
361
|
+
.pc-cal-event .t:empty { display: none; }
|
|
362
|
+
|
|
363
|
+
/* week view */
|
|
364
|
+
.pc-cal-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
|
|
365
|
+
.pc-cal-wday { min-width: 0; min-height: 150px; border-right: 1px solid var(--cal-line); padding: 10px 6px; }
|
|
366
|
+
.pc-cal-wday:last-child { border-right: none; }
|
|
367
|
+
.pc-cal-wday .pc-cal-dow { display: block; border-bottom: none; padding: 0; }
|
|
368
|
+
.pc-cal-wday .pc-cal-date { margin: 2px auto 4px; font-size: 15px; }
|
|
369
|
+
.pc-cal-wday .whead { text-align: center; }
|
|
370
|
+
|
|
371
|
+
/* upcoming view */
|
|
372
|
+
.pc-cal-agenda { list-style: none; margin: 0; padding: 6px 0; }
|
|
373
|
+
.pc-cal-agenda li + li { border-top: 1px solid var(--cal-line); }
|
|
374
|
+
.pc-cal-row {
|
|
375
|
+
display: flex; width: 100%; gap: 14px; align-items: baseline; padding: 11px 16px;
|
|
376
|
+
border: 0; background: none; color: var(--cal-ink); font-family: var(--font-body);
|
|
377
|
+
font-size: 13.5px; text-align: left; cursor: pointer;
|
|
378
|
+
}
|
|
379
|
+
.pc-cal-row:hover { background: var(--cal-soft); }
|
|
380
|
+
.pc-cal-row .d {
|
|
381
|
+
flex: 0 0 96px; font-weight: 700; font-size: 12px; color: var(--cal-muted);
|
|
382
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
383
|
+
}
|
|
384
|
+
.pc-calendar.is-multi .pc-cal-row .d::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 99px; background: var(--cal-dot, var(--cal-accent)); margin-right: 7px; vertical-align: 1px; }
|
|
385
|
+
.pc-cal-row .t { color: var(--cal-muted); font-variant-numeric: tabular-nums; margin-left: auto; font-size: 12.5px; }
|
|
386
|
+
|
|
387
|
+
/* footer: the Google Calendar link lives alone on a quiet line */
|
|
388
|
+
.pc-cal-foot { display: flex; justify-content: flex-end; padding: 9px 16px; border-top: 1px solid var(--cal-line); }
|
|
389
|
+
.pc-cal-open { font-size: 12px; font-weight: 600; color: var(--cal-muted); text-decoration: none; }
|
|
390
|
+
.pc-cal-open:hover { color: var(--cal-accent); }
|
|
391
|
+
|
|
392
|
+
/* event details popover */
|
|
393
|
+
.pc-cal-pop {
|
|
394
|
+
position: absolute; z-index: 5; max-width: 300px; padding: 12px 14px 12px;
|
|
395
|
+
background: var(--cal-bg); border: 1px solid var(--cal-line); border-radius: 10px;
|
|
396
|
+
box-shadow: 0 10px 28px rgb(0 0 0 / 14%); font-size: 12.5px; color: var(--cal-ink);
|
|
397
|
+
}
|
|
398
|
+
.pc-cal-pop-title { display: block; font-size: 13.5px; padding-right: 20px; }
|
|
399
|
+
.pc-cal-pop-when { color: var(--cal-muted); margin-top: 3px; font-variant-numeric: tabular-nums; }
|
|
400
|
+
.pc-cal-pop-loc { color: var(--cal-muted); margin-top: 3px; }
|
|
401
|
+
.pc-cal-pop-desc { margin: 8px 0 0; color: var(--cal-ink); line-height: 1.5; white-space: pre-line; }
|
|
402
|
+
.pc-cal-pop-x {
|
|
403
|
+
position: absolute; top: 6px; right: 6px; border: 0; background: none; color: var(--cal-muted);
|
|
404
|
+
font-size: 15px; line-height: 1; padding: 4px; cursor: pointer;
|
|
405
|
+
}
|
|
406
|
+
.pc-cal-pop-x:hover { color: var(--cal-ink); }
|
|
407
|
+
|
|
408
|
+
.pc-cal-empty { color: var(--cal-muted); font-size: 14px; text-align: center; padding: 26px 16px; margin: 0; }
|
|
409
|
+
|
|
410
|
+
@media (max-width: 640px) {
|
|
411
|
+
/* month cells are too narrow for text: events collapse to tappable dots
|
|
412
|
+
under the date, like the Google mobile grid */
|
|
413
|
+
.pc-cal-day { min-height: 52px; padding: 4px 2px; }
|
|
414
|
+
.pc-cal-grid .pc-cal-event {
|
|
415
|
+
width: 7px; height: 7px; padding: 0; margin: 4px auto 0; border-radius: 99px;
|
|
416
|
+
background: var(--cal-dot, var(--cal-accent)); overflow: hidden;
|
|
417
|
+
}
|
|
418
|
+
.pc-cal-grid .pc-cal-event .t, .pc-cal-grid .pc-cal-event b, .pc-cal-grid .pc-cal-event::before { display: none; }
|
|
419
|
+
.pc-cal-grid .pc-cal-event:nth-of-type(n + 4) { display: none; } /* max 3 dots per day */
|
|
420
|
+
.pc-cal-week { grid-template-columns: 1fr; }
|
|
421
|
+
.pc-cal-wday { min-height: 0; border-right: none; border-bottom: 1px solid var(--cal-line); display: flex; gap: 12px; padding: 10px 12px; }
|
|
422
|
+
.pc-cal-wday:last-child { border-bottom: none; }
|
|
423
|
+
.pc-cal-wday .whead { flex: 0 0 64px; }
|
|
424
|
+
.pc-cal-wday .wevs { flex: 1; min-width: 0; }
|
|
425
|
+
.pc-cal-row .d { flex-basis: 84px; }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Editorial theme — coded homepage. This is the "agent path": edit this file
|
|
3
|
+
// (and theme.css) to redesign the homepage; the words stay UI-editable because
|
|
4
|
+
// the isHome page's rendered body is injected as the hero. Sections are driven
|
|
5
|
+
// by data: `featured` posts, latest, and the `projects` / `log` tags.
|
|
6
|
+
import type { Page, Post, Settings, Tag } from '../../../lib/schema';
|
|
7
|
+
import { slugify } from '../../../lib/schema';
|
|
8
|
+
import { formatDate } from '../../../lib/dates';
|
|
9
|
+
import { CONTENT, postsIndexUrl, postUrl, tagUrl } from '../../../lib/content';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
settings: Settings;
|
|
13
|
+
page: Page | null;
|
|
14
|
+
posts: Post[]; // published, newest first
|
|
15
|
+
tags: Tag[];
|
|
16
|
+
}
|
|
17
|
+
const { page, posts, tags, settings } = Astro.props;
|
|
18
|
+
|
|
19
|
+
const tagSlug = (name: string) =>
|
|
20
|
+
tags.find((t) => t.name.toLowerCase() === name.toLowerCase())?.slug ?? slugify(name);
|
|
21
|
+
const hasTag = (p: Post, name: string) => p.tags.some((t) => t.toLowerCase() === name.toLowerCase());
|
|
22
|
+
|
|
23
|
+
// single-feature rule: at most one post is featured at a time
|
|
24
|
+
const feature = posts.find((p) => p.featured) ?? null;
|
|
25
|
+
const latest = posts.filter((p) => p.id !== feature?.id).slice(0, 4);
|
|
26
|
+
const projects = posts.filter((p) => hasTag(p, 'projects') || hasTag(p, 'project'));
|
|
27
|
+
const log = posts.filter((p) => hasTag(p, 'log') || hasTag(p, 'the log'));
|
|
28
|
+
const sectioned = new Set([...(feature ? [feature] : []), ...projects, ...log].map((p) => p.id));
|
|
29
|
+
const other = posts.filter((p) => !sectioned.has(p.id));
|
|
30
|
+
|
|
31
|
+
const fmt = (ts: number | null) => formatDate(ts, settings, 'short');
|
|
32
|
+
const readMins = (p: Post) =>
|
|
33
|
+
Math.max(1, Math.round((p.bodyHtml || p.body).replace(/<[^>]+>/g, ' ').split(/\s+/).filter(Boolean).length / 220));
|
|
34
|
+
// date and read time joined without stray separators when the date is missing
|
|
35
|
+
const metaLine = (p: Post) => [fmt(p.publishedAt), `${readMins(p)} min read`].filter(Boolean).join(' · ');
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
<div class="ed-home">
|
|
39
|
+
{page?.bodyHtml && <section class="ed-hero" set:html={page.bodyHtml} />}
|
|
40
|
+
|
|
41
|
+
{feature && (
|
|
42
|
+
<section class="ed-section ed-feature">
|
|
43
|
+
<div class="ed-sechead"><h2>Featured</h2><span class="ed-meta">01</span></div>
|
|
44
|
+
<article>
|
|
45
|
+
{feature.cover && (
|
|
46
|
+
<a class="ed-imglink" href={postUrl(feature.slug)}><img src={feature.cover} alt={feature.coverAlt} loading="lazy" /></a>
|
|
47
|
+
)}
|
|
48
|
+
<div class="ed-feature-text">
|
|
49
|
+
<div class="ed-meta">{metaLine(feature)}</div>
|
|
50
|
+
<h3><a href={postUrl(feature.slug)}>{feature.title}</a></h3>
|
|
51
|
+
{feature.excerpt && <p class="ed-excerpt">{feature.excerpt}</p>}
|
|
52
|
+
</div>
|
|
53
|
+
</article>
|
|
54
|
+
</section>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
<section class="ed-section">
|
|
58
|
+
<div class="ed-sechead"><h2>Latest</h2><a href={postsIndexUrl()}>View all {CONTENT.posts.plural.toLowerCase()}</a></div>
|
|
59
|
+
{latest.length === 0 ? (
|
|
60
|
+
<div class="ed-empty">No {CONTENT.posts.plural.toLowerCase()} yet.</div>
|
|
61
|
+
) : (
|
|
62
|
+
<ul class="ed-list">
|
|
63
|
+
{latest.map((p) => (
|
|
64
|
+
<li class="ed-item">
|
|
65
|
+
<div class="ed-meta">{metaLine(p)}</div>
|
|
66
|
+
<h3><a href={postUrl(p.slug)}>{p.title}</a></h3>
|
|
67
|
+
{p.excerpt && <p>{p.excerpt}</p>}
|
|
68
|
+
</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
)}
|
|
72
|
+
</section>
|
|
73
|
+
|
|
74
|
+
{(projects.length > 0 || log.length > 0) && (
|
|
75
|
+
<div class="ed-cols">
|
|
76
|
+
{projects.length > 0 && (
|
|
77
|
+
<section class="ed-section">
|
|
78
|
+
<div class="ed-sechead"><h2>Projects</h2><a href={tagUrl(tagSlug('projects'))}>View all</a></div>
|
|
79
|
+
<div class="ed-cards">
|
|
80
|
+
{projects.slice(0, 3).map((p) => (
|
|
81
|
+
<article class="ed-card">
|
|
82
|
+
{p.cover && (
|
|
83
|
+
<a class="ed-imglink" href={postUrl(p.slug)}><img src={p.cover} alt={p.coverAlt} loading="lazy" /></a>
|
|
84
|
+
)}
|
|
85
|
+
<div class="ed-meta">{metaLine(p)}</div>
|
|
86
|
+
<h3><a href={postUrl(p.slug)}>{p.title}</a></h3>
|
|
87
|
+
{p.excerpt && <p class="ed-excerpt">{p.excerpt}</p>}
|
|
88
|
+
</article>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</section>
|
|
92
|
+
)}
|
|
93
|
+
{log.length > 0 && (
|
|
94
|
+
<section class="ed-section">
|
|
95
|
+
<div class="ed-sechead"><h2>The Log</h2><a href={tagUrl(tagSlug('log'))}>View all</a></div>
|
|
96
|
+
<div class="ed-cards">
|
|
97
|
+
{log.slice(0, 3).map((p) => (
|
|
98
|
+
<article class="ed-card">
|
|
99
|
+
{p.cover && (
|
|
100
|
+
<a class="ed-imglink" href={postUrl(p.slug)}><img src={p.cover} alt={p.coverAlt} loading="lazy" /></a>
|
|
101
|
+
)}
|
|
102
|
+
<div class="ed-meta">{metaLine(p)}</div>
|
|
103
|
+
<h3><a href={postUrl(p.slug)}>{p.title}</a></h3>
|
|
104
|
+
{p.excerpt && <p class="ed-excerpt">{p.excerpt}</p>}
|
|
105
|
+
</article>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{other.length > 0 && (
|
|
114
|
+
<section class="ed-section">
|
|
115
|
+
<div class="ed-sechead"><h2>Other</h2><a href={postsIndexUrl()}>View all {CONTENT.posts.plural.toLowerCase()}</a></div>
|
|
116
|
+
<ul class="ed-list">
|
|
117
|
+
{other.slice(0, 4).map((p) => (
|
|
118
|
+
<li class="ed-item">
|
|
119
|
+
<div class="ed-meta">{metaLine(p)}</div>
|
|
120
|
+
<h3><a href={postUrl(p.slug)}>{p.title}</a></h3>
|
|
121
|
+
{p.excerpt && <p>{p.excerpt}</p>}
|
|
122
|
+
</li>
|
|
123
|
+
))}
|
|
124
|
+
</ul>
|
|
125
|
+
</section>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Editorial theme — "minimal" post template, selectable per-post in the
|
|
3
|
+
// editor sidebar. A stripped-back reading view: no cover, no author card, no
|
|
4
|
+
// related section — just the title, a dateline, and the words.
|
|
5
|
+
//
|
|
6
|
+
// Template contract (any templates/post-<name>.astro receives the same):
|
|
7
|
+
// post — the Post (title, publishedAt, tags, …)
|
|
8
|
+
// html — rendered body HTML, dynamic blocks already expanded
|
|
9
|
+
// authors — ordered User[] (may be empty)
|
|
10
|
+
// settings — site Settings
|
|
11
|
+
// related — up to 3 related Post[]
|
|
12
|
+
import type { Post, Settings, User } from '../../../lib/schema';
|
|
13
|
+
import { authorSlug } from '../../../lib/schema';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
post: Post;
|
|
17
|
+
html: string;
|
|
18
|
+
authors: User[];
|
|
19
|
+
settings: Settings;
|
|
20
|
+
related: Post[];
|
|
21
|
+
}
|
|
22
|
+
const { post, html, authors } = Astro.props;
|
|
23
|
+
const fmt = (ts: number | null) =>
|
|
24
|
+
ts ? new Date(ts).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }) : '';
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<article class="tpl-minimal">
|
|
28
|
+
<header>
|
|
29
|
+
<h1>{post.title}</h1>
|
|
30
|
+
<p class="tpl-minimal-meta">
|
|
31
|
+
{post.publishedAt && <time datetime={new Date(post.publishedAt).toISOString()}>{fmt(post.publishedAt)}</time>}
|
|
32
|
+
{authors.length > 0 && (
|
|
33
|
+
<span>
|
|
34
|
+
{post.publishedAt && ' · '}by{' '}
|
|
35
|
+
{authors.map((a, i) => (
|
|
36
|
+
<>{i > 0 && (i === authors.length - 1 ? ' & ' : ', ')}<a href={`/author/${authorSlug(a)}`}>{a.name}</a></>
|
|
37
|
+
))}
|
|
38
|
+
</span>
|
|
39
|
+
)}
|
|
40
|
+
</p>
|
|
41
|
+
</header>
|
|
42
|
+
<div class="post-content" set:html={html} />
|
|
43
|
+
</article>
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.tpl-minimal { max-width: 62ch; margin: 0 auto; }
|
|
47
|
+
.tpl-minimal h1 { font-size: 2rem; line-height: 1.2; margin: 2rem 0 0.5rem; }
|
|
48
|
+
.tpl-minimal-meta { color: var(--muted); font-size: 0.9rem; margin-bottom: 2.5rem; }
|
|
49
|
+
.tpl-minimal-meta a { color: inherit; }
|
|
50
|
+
</style>
|