@half-built/astro 0.5.0 → 0.7.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/README.md +33 -2
- package/package.json +11 -4
- package/src/components/CategoryCard.astro +9 -1
- package/src/components/CornerBadges.astro +14 -21
- package/src/components/Footer.astro +68 -46
- package/src/components/Icon.astro +16 -0
- package/src/components/LightboxLink.astro +19 -10
- package/src/components/LinkListWidget.astro +11 -9
- package/src/components/Pagination.astro +51 -28
- package/src/components/PostCard.astro +54 -22
- package/src/components/PostNavigation.astro +40 -29
- package/src/components/Shell.astro +26 -12
- package/src/components/SiteHeader.astro +139 -80
- package/src/components/SmartImage.astro +48 -17
- package/src/components/Subscribe.astro +60 -35
- package/src/components/ThemeToggle.astro +28 -15
- package/src/components/TwoColumn.astro +5 -5
- package/src/components/Widget.astro +10 -7
- package/src/components/content/BlogImage.astro +48 -13
- package/src/components/content/Button.astro +13 -13
- package/src/components/content/Callout.astro +17 -8
- package/src/components/content/CodeBlock.astro +6 -1
- package/src/components/content/EditorNote.astro +19 -21
- package/src/components/content/Gallery.astro +18 -5
- package/src/components/content/GalleryImage.astro +26 -7
- package/src/components/content/Group.astro +5 -6
- package/src/components/content/MediaText.astro +43 -9
- package/src/components/content/Palette.astro +37 -21
- package/src/components/content/PostLink.astro +8 -11
- package/src/components/content/Quote.astro +10 -9
- package/src/components/content/Spacer.astro +11 -2
- package/src/components/content/Step.astro +34 -15
- package/src/components/content/Walkthrough.astro +10 -6
- package/src/components/content/WhenPublished.astro +7 -15
- package/src/components/models.ts +2 -2
- package/src/lib/archive.ts +18 -4
- package/src/lib/drafts.ts +16 -5
- package/src/lib/format-date.ts +4 -1
- package/src/lib/ordering.ts +3 -1
- package/src/lib/paginate.ts +5 -1
- package/src/lib/slug.ts +30 -7
- package/src/scripts/code-island.ts +27 -4
- package/src/scripts/core/badges.ts +28 -0
- package/src/scripts/core/dom.ts +6 -1
- package/src/scripts/core/frame-loop.ts +4 -1
- package/src/scripts/core/icons.ts +92 -15
- package/src/scripts/ecosystem.ts +72 -12
- package/src/scripts/focus-mode.ts +14 -3
- package/src/scripts/lightbox.ts +227 -41
- package/src/scripts/link-tip.ts +55 -11
- package/src/scripts/path-player-math.ts +11 -2
- package/src/scripts/path-player-paint.ts +54 -8
- package/src/scripts/path-player.ts +131 -29
- package/src/scripts/plate-modal.ts +45 -9
- package/src/scripts/scroll-top.ts +6 -1
- package/src/scripts/site-header.ts +34 -6
- package/src/scripts/subscribe.ts +50 -9
- package/src/scripts/theme-toggle.ts +32 -5
- package/src/shiki/code-theme.mjs +25 -5
- package/src/shiki/code-vars.mjs +5 -1
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
---
|
|
2
|
-
/* Archive/grid post card matching the live markup exactly:
|
|
3
|
-
article > .post-item.post-grid > .post-item-image (thumbnail link + reading
|
|
4
|
-
chip) + .post-item-content (categories, underlined title, meta, excerpt).
|
|
5
|
-
Comments are permanently dropped, so the chip carries read time only.
|
|
6
|
-
Fed by postCardModel() in src/lib/view-models.ts. */
|
|
7
2
|
import SmartImage from "./SmartImage.astro";
|
|
8
3
|
import CornerBadges from "./CornerBadges.astro";
|
|
4
|
+
import Icon from "./Icon.astro";
|
|
9
5
|
import type { PostCardModel } from "./models";
|
|
10
6
|
|
|
11
|
-
interface Props extends PostCardModel {
|
|
7
|
+
interface Props extends PostCardModel {
|
|
8
|
+
headingLevel?: 2 | 3 | 4;
|
|
9
|
+
class?: string;
|
|
10
|
+
minReadLabel?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
12
13
|
const {
|
|
13
|
-
href,
|
|
14
|
-
|
|
14
|
+
href,
|
|
15
|
+
title,
|
|
16
|
+
excerpt,
|
|
17
|
+
author,
|
|
18
|
+
dateStr,
|
|
19
|
+
minutes,
|
|
20
|
+
hero,
|
|
21
|
+
heroPosition,
|
|
22
|
+
draft = false,
|
|
23
|
+
badges,
|
|
24
|
+
categories,
|
|
25
|
+
headingLevel = 2,
|
|
26
|
+
class: className,
|
|
15
27
|
minReadLabel = "min read",
|
|
16
28
|
} = Astro.props;
|
|
17
|
-
|
|
18
|
-
compiler drop the Props type (found 2026-08-25, bisected with
|
|
19
|
-
convertToTSX). */
|
|
29
|
+
|
|
20
30
|
const Heading: "h2" | "h3" | "h4" = `h${headingLevel}`;
|
|
21
|
-
/* Optional per-post crop focus for the cover-fit thumbnail; see the
|
|
22
|
-
heroPosition comment in content.config.ts. */
|
|
23
31
|
const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
24
32
|
---
|
|
33
|
+
|
|
25
34
|
{/* The mapper guarantees an image (Henry placeholder when no featured
|
|
26
35
|
image), so every card takes the thumbnail treatment. */}
|
|
27
36
|
<article class:list={["has-post-thumbnail", className]}>
|
|
@@ -29,15 +38,21 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
29
38
|
<div class="post-item-image">
|
|
30
39
|
{/* alt divergence kept from pre-step-5: empty on the svg placeholder, title on a real hero; the anchor is aria-hidden either way. */}
|
|
31
40
|
<a class="post-thumbnail" href={href} aria-hidden="true" tabindex="-1">
|
|
32
|
-
<SmartImage
|
|
41
|
+
<SmartImage
|
|
42
|
+
src={hero}
|
|
43
|
+
alt={hero.format === "svg" ? "" : title}
|
|
44
|
+
widths={[470, 940]}
|
|
45
|
+
sizes="(max-width: 700px) 100vw, 470px"
|
|
46
|
+
style={heroStyle}
|
|
47
|
+
/>
|
|
33
48
|
</a>
|
|
34
49
|
{draft && <span class="draft-stamp">DRAFT</span>}
|
|
35
50
|
<div class="read-time-comment">
|
|
36
51
|
<span class="reading-time chip">
|
|
37
|
-
<
|
|
52
|
+
<Icon name="clock" size={12} />
|
|
38
53
|
{`${minutes} ${minReadLabel}`}</span>
|
|
39
54
|
</div>
|
|
40
|
-
<CornerBadges
|
|
55
|
+
<CornerBadges badges={badges} />
|
|
41
56
|
</div>
|
|
42
57
|
<div class="post-item-content">
|
|
43
58
|
<div class="entry-cat">
|
|
@@ -45,19 +60,23 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
45
60
|
category list onto extra rows; ours comma-joins on one line and
|
|
46
61
|
lets the browser ellipsize overflow. Do not restore the flex ul. */}
|
|
47
62
|
<span class="post-categories">
|
|
48
|
-
{categories.map((c, idx) =>
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
{categories.map((c, idx) => <Fragment
|
|
64
|
+
>{idx > 0 && ", "}<a href={c.href} rel="category tag"
|
|
65
|
+
>{c.name}</a
|
|
66
|
+
></Fragment
|
|
67
|
+
>)}
|
|
51
68
|
</span>
|
|
52
69
|
</div>
|
|
53
|
-
<Heading class="entry-title"
|
|
70
|
+
<Heading class="entry-title"
|
|
71
|
+
><a href={href} rel="bookmark">{title}</a></Heading
|
|
72
|
+
>
|
|
54
73
|
<ul class="entry-meta">
|
|
55
74
|
<li class="post-author">
|
|
56
|
-
<
|
|
75
|
+
<Icon name="user" size={11} />
|
|
57
76
|
{author}
|
|
58
77
|
</li>
|
|
59
78
|
<li class="post-date">
|
|
60
|
-
<
|
|
79
|
+
<Icon name="calendar" size={11} />
|
|
61
80
|
{dateStr}
|
|
62
81
|
</li>
|
|
63
82
|
</ul>
|
|
@@ -70,12 +89,15 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
70
89
|
|
|
71
90
|
<style>
|
|
72
91
|
.post-item { transition: all 0.5s ease-in-out; }
|
|
92
|
+
|
|
73
93
|
/* Same outline as MediaText media and gallery plates (owner request);
|
|
74
94
|
on the container so the hover zoom stays clipped inside it. */
|
|
75
95
|
.post-item-image { position: relative; overflow: hidden; border: var(--stroke) solid var(--rule); }
|
|
76
96
|
.post-item-image a { display: block; height: 100%; width: 100%; line-height: 0; }
|
|
97
|
+
|
|
77
98
|
/* The image box clips overflow, so the site-wide ring sits inside. */
|
|
78
99
|
.post-item-image a:focus-visible { outline-offset: -3px; }
|
|
100
|
+
|
|
79
101
|
.post-item-image a :global(img) {
|
|
80
102
|
width: 100%;
|
|
81
103
|
height: 100%;
|
|
@@ -84,12 +106,14 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
84
106
|
transition: all 0.5s ease-in-out;
|
|
85
107
|
}
|
|
86
108
|
.post-item:hover .post-item-image :global(img) { transform: scale(1.07); }
|
|
109
|
+
|
|
87
110
|
/* Stamp from the .draft-stamp pattern; the card's size and stacking
|
|
88
111
|
(above the hover-zoomed image) are here. */
|
|
89
112
|
.draft-stamp {
|
|
90
113
|
z-index: 2;
|
|
91
114
|
font-size: var(--font-size-xl);
|
|
92
115
|
}
|
|
116
|
+
|
|
93
117
|
/* Chip look comes from the .chip pattern; only placement is
|
|
94
118
|
contextual. display: flex, not a plain block: a block wrapper
|
|
95
119
|
gives the inline-flex chip a text line box whose descender gap
|
|
@@ -104,11 +128,13 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
104
128
|
display: flex;
|
|
105
129
|
}
|
|
106
130
|
.post-item-content { padding: 15px 0 0; padding-top: 10px; }
|
|
131
|
+
|
|
107
132
|
.entry-cat .post-categories {
|
|
108
133
|
display: block;
|
|
109
134
|
white-space: nowrap;
|
|
110
135
|
overflow: hidden;
|
|
111
136
|
text-overflow: ellipsis;
|
|
137
|
+
|
|
112
138
|
/* The links' font box stands taller than this line-height:1 row, and
|
|
113
139
|
overflow clips at the padding box, so without vertical breathing
|
|
114
140
|
room the inset focus/hover ring loses its top and bottom bands.
|
|
@@ -123,16 +149,19 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
123
149
|
line-height: 1; /* carried over from the old li a rule; keeps card height */
|
|
124
150
|
letter-spacing: 0.8px;
|
|
125
151
|
}
|
|
152
|
+
|
|
126
153
|
.entry-cat .post-categories a {
|
|
127
154
|
color: inherit;
|
|
128
155
|
text-decoration: none;
|
|
129
156
|
transition: var(--transition);
|
|
130
157
|
}
|
|
158
|
+
|
|
131
159
|
.entry-cat .post-categories a:hover {
|
|
132
160
|
outline: 2px solid currentColor;
|
|
133
161
|
outline-offset: -2px; /* Draws outline inside link to avoid clipping by container overflow */
|
|
134
162
|
}
|
|
135
163
|
.entry-cat .post-categories a:focus-visible { outline-offset: -2px; }
|
|
164
|
+
|
|
136
165
|
/* Owner-approved divergence from live (2026-07-26): the live theme
|
|
137
166
|
word-breaks titles mid-word (word-break: break-all); ours wraps at
|
|
138
167
|
word boundaries on purpose. Do not add break-all for fidelity. */
|
|
@@ -142,6 +171,7 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
142
171
|
font-size: var(--font-size-base);
|
|
143
172
|
font-family: var(--font-body);
|
|
144
173
|
}
|
|
174
|
+
|
|
145
175
|
.entry-title a {
|
|
146
176
|
color: var(--ink);
|
|
147
177
|
display: inline;
|
|
@@ -149,6 +179,7 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
149
179
|
transition: var(--transition);
|
|
150
180
|
}
|
|
151
181
|
.entry-title a:hover { outline: 2px solid currentColor; }
|
|
182
|
+
|
|
152
183
|
.entry-meta {
|
|
153
184
|
margin: 0 0 10px;
|
|
154
185
|
padding: 0;
|
|
@@ -159,6 +190,7 @@ const heroStyle = heroPosition ? `object-position: ${heroPosition}` : undefined;
|
|
|
159
190
|
column-gap: 10px;
|
|
160
191
|
align-items: center;
|
|
161
192
|
}
|
|
193
|
+
|
|
162
194
|
.entry-meta li {
|
|
163
195
|
margin-bottom: 0;
|
|
164
196
|
font-size: var(--font-size-xs);
|
|
@@ -1,51 +1,61 @@
|
|
|
1
1
|
---
|
|
2
|
-
/* Prev/next post links: nav.navigation.post-navigation with
|
|
3
|
-
nav-previous/nav-next and Previous:/Next: subtitles, in the theme's
|
|
4
|
-
3px-bordered box. Previous = older post, Next = newer, as WordPress does.
|
|
5
|
-
Owner divergence from the live markup (2026-07-30): the newer post sits
|
|
6
|
-
LEFT and the older post RIGHT, matching the newest-first reading order
|
|
7
|
-
of the archives. Do not restore the WP left/right order for fidelity. */
|
|
8
2
|
import type { PostRef } from "./models";
|
|
3
|
+
|
|
9
4
|
interface Props {
|
|
10
5
|
class?: string;
|
|
11
6
|
prev?: PostRef;
|
|
12
7
|
next?: PostRef;
|
|
13
8
|
labels?: { heading?: string; next?: string; prev?: string; nav?: string };
|
|
14
9
|
}
|
|
15
|
-
|
|
16
|
-
const
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
prev,
|
|
13
|
+
next,
|
|
14
|
+
class: className,
|
|
15
|
+
labels: labelsOverride = {},
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
|
|
18
|
+
const labels = {
|
|
19
|
+
heading: "Post navigation",
|
|
20
|
+
next: "Next:",
|
|
21
|
+
prev: "Previous:",
|
|
22
|
+
nav: "Posts",
|
|
23
|
+
...labelsOverride,
|
|
24
|
+
};
|
|
17
25
|
---
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
</
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</
|
|
35
|
-
|
|
36
|
-
</div>
|
|
37
|
-
</
|
|
38
|
-
|
|
26
|
+
|
|
27
|
+
{(prev || next) && <nav
|
|
28
|
+
class:list={["navigation", "post-navigation", className]}
|
|
29
|
+
aria-label={labels.nav}
|
|
30
|
+
>
|
|
31
|
+
<h2 class="screen-reader-text">{labels.heading}</h2>
|
|
32
|
+
<div class="nav-links">
|
|
33
|
+
{next && <div class="nav-next">
|
|
34
|
+
<a href={next.href} rel="next">
|
|
35
|
+
<span class="nav-subtitle">{labels.next}</span>
|
|
36
|
+
<span class="nav-title">{next.title}</span>
|
|
37
|
+
</a>
|
|
38
|
+
</div>}
|
|
39
|
+
{prev && <div class="nav-previous">
|
|
40
|
+
<a href={prev.href} rel="prev">
|
|
41
|
+
<span class="nav-subtitle">{labels.prev}</span>
|
|
42
|
+
<span class="nav-title">{prev.title}</span>
|
|
43
|
+
</a>
|
|
44
|
+
</div>}
|
|
45
|
+
</div>
|
|
46
|
+
</nav>}
|
|
39
47
|
|
|
40
48
|
<style>
|
|
41
49
|
.post-navigation {
|
|
42
50
|
margin-top: 30px;
|
|
43
51
|
padding: 20px;
|
|
52
|
+
|
|
44
53
|
/* 3px is this box's design constant (the live theme's weight),
|
|
45
54
|
deliberately NOT var(--stroke): it does not scale with the theme
|
|
46
55
|
stroke. */
|
|
47
56
|
border: var(--stroke-3) solid var(--rule);
|
|
48
57
|
}
|
|
58
|
+
|
|
49
59
|
.nav-links {
|
|
50
60
|
display: flex;
|
|
51
61
|
justify-content: space-between;
|
|
@@ -55,6 +65,7 @@ const labels = { heading: "Post navigation", next: "Next:", prev: "Previous:", n
|
|
|
55
65
|
}
|
|
56
66
|
.nav-previous { text-align: right; margin-left: auto; }
|
|
57
67
|
.nav-links a { text-decoration: none; color: var(--ink); }
|
|
68
|
+
|
|
58
69
|
.nav-links a:hover .nav-title,
|
|
59
70
|
.nav-links a:focus .nav-title { text-decoration: underline; }
|
|
60
71
|
.nav-subtitle { font-weight: bold; }
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
lang?: string;
|
|
6
|
+
skipLabel?: string;
|
|
7
|
+
scrollTopLabel?: string;
|
|
8
|
+
themeStorageKey: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
lang = "en",
|
|
13
|
+
skipLabel = "Skip to content",
|
|
14
|
+
scrollTopLabel = "Scroll to top",
|
|
15
|
+
themeStorageKey,
|
|
16
|
+
} = Astro.props;
|
|
11
17
|
---
|
|
18
|
+
|
|
12
19
|
<!doctype html>
|
|
13
20
|
<html lang={lang}>
|
|
14
21
|
<head>
|
|
@@ -34,7 +41,9 @@ const { lang = "en", skipLabel = "Skip to content", scrollTopLabel = "Scroll to
|
|
|
34
41
|
</head>
|
|
35
42
|
<body>
|
|
36
43
|
<div id="page" class="site">
|
|
37
|
-
<a class="skip-link screen-reader-text" href="#primary-content"
|
|
44
|
+
<a class="skip-link screen-reader-text" href="#primary-content"
|
|
45
|
+
>{skipLabel}</a
|
|
46
|
+
>
|
|
38
47
|
<slot name="header" />
|
|
39
48
|
<div id="primary-content" class="primary-site-content">
|
|
40
49
|
<div class="site-content shell">
|
|
@@ -42,8 +51,13 @@ const { lang = "en", skipLabel = "Skip to content", scrollTopLabel = "Scroll to
|
|
|
42
51
|
</div>
|
|
43
52
|
</div>
|
|
44
53
|
<slot name="footer" />
|
|
45
|
-
<a
|
|
46
|
-
|
|
54
|
+
<a
|
|
55
|
+
href="#page"
|
|
56
|
+
id="scroll-to-top"
|
|
57
|
+
class="scroll-top"
|
|
58
|
+
aria-label={scrollTopLabel}
|
|
59
|
+
>
|
|
60
|
+
<Icon name="chevron-up" size={14} />
|
|
47
61
|
</a>
|
|
48
62
|
</div>
|
|
49
63
|
{/* Site behavior scripts land here, inside the body, where the pre-step-8 markup carried them. */}
|