@half-built/astro 0.5.0 → 0.6.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.
Files changed (60) hide show
  1. package/README.md +33 -2
  2. package/package.json +11 -4
  3. package/src/components/CategoryCard.astro +9 -1
  4. package/src/components/CornerBadges.astro +14 -21
  5. package/src/components/Footer.astro +57 -29
  6. package/src/components/Icon.astro +16 -0
  7. package/src/components/LightboxLink.astro +19 -10
  8. package/src/components/LinkListWidget.astro +11 -9
  9. package/src/components/Pagination.astro +51 -28
  10. package/src/components/PostCard.astro +54 -22
  11. package/src/components/PostNavigation.astro +40 -29
  12. package/src/components/Shell.astro +26 -12
  13. package/src/components/SiteHeader.astro +139 -80
  14. package/src/components/SmartImage.astro +48 -17
  15. package/src/components/Subscribe.astro +60 -35
  16. package/src/components/ThemeToggle.astro +28 -15
  17. package/src/components/TwoColumn.astro +5 -5
  18. package/src/components/Widget.astro +10 -7
  19. package/src/components/content/BlogImage.astro +48 -13
  20. package/src/components/content/Button.astro +13 -13
  21. package/src/components/content/Callout.astro +17 -8
  22. package/src/components/content/CodeBlock.astro +6 -1
  23. package/src/components/content/EditorNote.astro +19 -21
  24. package/src/components/content/Gallery.astro +18 -5
  25. package/src/components/content/GalleryImage.astro +26 -7
  26. package/src/components/content/Group.astro +5 -6
  27. package/src/components/content/MediaText.astro +43 -9
  28. package/src/components/content/Palette.astro +37 -21
  29. package/src/components/content/PostLink.astro +8 -11
  30. package/src/components/content/Quote.astro +10 -9
  31. package/src/components/content/Spacer.astro +11 -2
  32. package/src/components/content/Step.astro +34 -15
  33. package/src/components/content/Walkthrough.astro +10 -6
  34. package/src/components/content/WhenPublished.astro +7 -15
  35. package/src/components/models.ts +2 -2
  36. package/src/lib/archive.ts +18 -4
  37. package/src/lib/drafts.ts +16 -5
  38. package/src/lib/format-date.ts +4 -1
  39. package/src/lib/ordering.ts +3 -1
  40. package/src/lib/paginate.ts +5 -1
  41. package/src/lib/slug.ts +30 -7
  42. package/src/scripts/code-island.ts +27 -4
  43. package/src/scripts/core/badges.ts +28 -0
  44. package/src/scripts/core/dom.ts +6 -1
  45. package/src/scripts/core/frame-loop.ts +4 -1
  46. package/src/scripts/core/icons.ts +92 -15
  47. package/src/scripts/ecosystem.ts +72 -12
  48. package/src/scripts/focus-mode.ts +14 -3
  49. package/src/scripts/lightbox.ts +227 -41
  50. package/src/scripts/link-tip.ts +55 -11
  51. package/src/scripts/path-player-math.ts +11 -2
  52. package/src/scripts/path-player-paint.ts +54 -8
  53. package/src/scripts/path-player.ts +131 -29
  54. package/src/scripts/plate-modal.ts +45 -9
  55. package/src/scripts/scroll-top.ts +6 -1
  56. package/src/scripts/site-header.ts +34 -6
  57. package/src/scripts/subscribe.ts +50 -9
  58. package/src/scripts/theme-toggle.ts +32 -5
  59. package/src/shiki/code-theme.mjs +25 -5
  60. 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 { headingLevel?: 2 | 3 | 4; class?: string; minReadLabel?: string }
7
+ interface Props extends PostCardModel {
8
+ headingLevel?: 2 | 3 | 4;
9
+ class?: string;
10
+ minReadLabel?: string;
11
+ }
12
+
12
13
  const {
13
- href, title, excerpt, author, dateStr, minutes, hero, heroPosition,
14
- draft = false, genai, demo, categories, headingLevel = 2, class: className,
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
- /* Annotated, not cast: an `as` cast in the frontmatter makes the Astro
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 src={hero} alt={hero.format === "svg" ? "" : title} widths={[470, 940]} sizes="(max-width: 700px) 100vw, 470px" style={heroStyle} />
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
- <svg viewBox="0 0 512 512" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"/></svg>
52
+ <Icon name="clock" size={12} />
38
53
  {`${minutes} ${minReadLabel}`}</span>
39
54
  </div>
40
- <CornerBadges genai={genai} demo={demo} />
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
- <Fragment>{idx > 0 && ", "}<a href={c.href} rel="category tag">{c.name}</a></Fragment>
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"><a href={href} rel="bookmark">{title}</a></Heading>
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
- <svg viewBox="0 0 448 512" width="11" height="11" fill="currentColor" aria-hidden="true"><path d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"/></svg>
75
+ <Icon name="user" size={11} />
57
76
  {author}
58
77
  </li>
59
78
  <li class="post-date">
60
- <svg viewBox="0 0 448 512" width="11" height="11" fill="currentColor" aria-hidden="true"><path d="M148 288h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm108-12v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm96 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm-96 96v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm-96 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm192 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm96-260v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h48V12c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h128V12c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h48c26.5 0 48 21.5 48 48z"/></svg>
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
- const { prev, next, class: className, labels: labelsOverride = {} } = Astro.props;
16
- const labels = { heading: "Post navigation", next: "Next:", prev: "Previous:", nav: "Posts", ...labelsOverride };
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
- {(prev || next) && (
19
- <nav class:list={["navigation", "post-navigation", className]} aria-label={labels.nav}>
20
- <h2 class="screen-reader-text">{labels.heading}</h2>
21
- <div class="nav-links">
22
- {next && (
23
- <div class="nav-next">
24
- <a href={next.href} rel="next">
25
- <span class="nav-subtitle">{labels.next}</span> <span class="nav-title">{next.title}</span>
26
- </a>
27
- </div>
28
- )}
29
- {prev && (
30
- <div class="nav-previous">
31
- <a href={prev.href} rel="prev">
32
- <span class="nav-subtitle">{labels.prev}</span> <span class="nav-title">{prev.title}</span>
33
- </a>
34
- </div>
35
- )}
36
- </div>
37
- </nav>
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
- /* The document shell (step 8, 2026-08-30): doctype and head skeleton,
3
- first-paint theme stamp, skip link, page frame, scroll-to-top
4
- floater. Site-specific head content (meta, OG, title) arrives through
5
- the head slot, the chrome through the header and footer slots, so a
6
- second site reuses the frame without forking it. No scoped CSS on
7
- purpose: the shell's layout rules live once in styles/base/shell.css
8
- (pinned by test/base.test.ts). */
9
- interface Props { lang?: string; skipLabel?: string; scrollTopLabel?: string; themeStorageKey: string }
10
- const { lang = "en", skipLabel = "Skip to content", scrollTopLabel = "Scroll to top", themeStorageKey } = Astro.props;
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">{skipLabel}</a>
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 href="#page" id="scroll-to-top" class="scroll-top" aria-label={scrollTopLabel}>
46
- <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 15 12 7 20 15" /></svg>
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. */}