@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
package/README.md
CHANGED
|
@@ -12,8 +12,39 @@ is the design record for this package.
|
|
|
12
12
|
|
|
13
13
|
## Icons
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Every glyph the package draws comes from one registry,
|
|
16
|
+
`scripts/core/icons.ts`, derived from Lucide (https://lucide.dev), ISC
|
|
17
|
+
license; see `ICONS-LICENSE`. Templates render one through
|
|
18
|
+
`components/Icon.astro`:
|
|
19
|
+
|
|
20
|
+
```astro
|
|
21
|
+
<Icon name="search" size={14} />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`name` is a registry key (`x`, `play`, `sparkles`, `pause`,
|
|
25
|
+
`rotate-ccw`, `chevron-left`, `chevron-right`, `chevron-up`,
|
|
26
|
+
`arrow-left`, `arrow-right`, `sun`, `moon`, `search`, `clock`, `user`,
|
|
27
|
+
`calendar`, `circle`), `size` a CSS
|
|
28
|
+
length or pixel count (default `1em`, tracking the parent's font
|
|
29
|
+
size), `strokeWidth` defaults to 2.5, and `class` lands on the svg.
|
|
30
|
+
The icon is decorative by contract (aria-hidden, pointer-events none),
|
|
31
|
+
so the accessible name belongs to the button or link around it. The
|
|
32
|
+
svg arrives through `set:html` and carries no scoped-style attribute;
|
|
33
|
+
style it from the parent with `:global(svg)`. Client scripts take the
|
|
34
|
+
`ICON_*` strings from the same file. Add a glyph to the registry,
|
|
35
|
+
never as inline `<svg>` in a component; a test enforces that.
|
|
36
|
+
|
|
37
|
+
## Corner badges
|
|
38
|
+
|
|
39
|
+
`CornerBadges.astro` takes its chips as data, `badges: Badge[]` from
|
|
40
|
+
`scripts/core/badges.ts`: a `key` (rendered as the `badge-<key>` class,
|
|
41
|
+
the styling hook), a `label`, and an `icon` name from the registry.
|
|
42
|
+
The two house conventions ship as `GENAI_BADGE` and `DEMO_BADGE`; a
|
|
43
|
+
site's view-model lists the badges a post carries, in order, and can
|
|
44
|
+
add its own without a package change. `PostCardModel.badges` carries
|
|
45
|
+
them to the card. The content components (`BlogImage`, `GalleryImage`,
|
|
46
|
+
`MediaText`) keep a `genai` boolean as authoring sugar for MDX and take
|
|
47
|
+
`badges` for anything else.
|
|
17
48
|
|
|
18
49
|
## EditorNote
|
|
19
50
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@half-built/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Astro components, islands, and pure helpers for the half-built design system.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"directory": "packages/astro"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
-
"files": [
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"ICONS-LICENSE"
|
|
15
|
+
],
|
|
13
16
|
"exports": {
|
|
14
17
|
"./components/Masthead.astro": "./src/components/SiteHeader.astro",
|
|
15
18
|
"./components/*": "./src/components/*",
|
|
@@ -19,6 +22,10 @@
|
|
|
19
22
|
"./shiki/code-theme": "./src/shiki/code-theme.mjs",
|
|
20
23
|
"./shiki/code-vars": "./src/shiki/code-vars.mjs"
|
|
21
24
|
},
|
|
22
|
-
"peerDependencies": {
|
|
23
|
-
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"astro": "^5.0.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
24
31
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ImageMetadata } from "astro";
|
|
3
3
|
|
|
4
|
-
interface Props {
|
|
4
|
+
interface Props {
|
|
5
|
+
name: string;
|
|
6
|
+
href: string;
|
|
7
|
+
image: ImageMetadata;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
const { name, href, image, class: className } = Astro.props;
|
|
6
12
|
const bg = `background-image:url('${image.src.replace(/'/g, "%27").replace(/"/g, "%22")}')`;
|
|
7
13
|
---
|
|
14
|
+
|
|
8
15
|
<a class:list={["category-single", className]} href={href}>
|
|
9
16
|
<span class="category-img" style={bg}></span>
|
|
10
17
|
<span class="category-label chip">{name}</span>
|
|
@@ -21,6 +28,7 @@ const bg = `background-image:url('${image.src.replace(/'/g, "%27").replace(/"/g,
|
|
|
21
28
|
text-decoration: none;
|
|
22
29
|
}
|
|
23
30
|
.category-img { position: absolute; inset: 0; background-size: cover; background-position: center; }
|
|
31
|
+
|
|
24
32
|
/* Chip look comes from the .chip pattern; card-specific metrics only. */
|
|
25
33
|
.category-label {
|
|
26
34
|
position: relative;
|
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
lower-right corner of the image they sit on.
|
|
5
|
-
- AI Art marks an AI-generated image: heroes through the heroGenAI
|
|
6
|
-
frontmatter flag, content images through the genai prop on
|
|
7
|
-
BlogImage, GalleryImage, and MediaText. Images that only went
|
|
8
|
-
through ordinary editing carry nothing.
|
|
9
|
-
- Demo marks a post that carries an interactive demo (the demo
|
|
10
|
-
frontmatter flag), on the card and the post hero.
|
|
11
|
-
Renders nothing when neither applies. The container is absolutely
|
|
12
|
-
positioned, so the parent supplies position: relative. pointer-events
|
|
13
|
-
off: a badge inside a lightbox link must not swallow the tap. */
|
|
14
|
-
import { ICON_PLAY, ICON_SPARKLES } from "../scripts/core/icons";
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
import type { Badge } from "../scripts/core/badges";
|
|
15
4
|
|
|
16
|
-
interface Props {
|
|
17
|
-
|
|
5
|
+
interface Props {
|
|
6
|
+
badges?: Badge[];
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { badges = [], class: className } = Astro.props;
|
|
18
11
|
---
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</span>
|
|
24
|
-
|
|
12
|
+
|
|
13
|
+
{badges.length > 0 && <span class:list={["corner-badges", className]}>
|
|
14
|
+
{badges.map((badge) => <span class:list={["chip", `badge-${badge.key}`]}>
|
|
15
|
+
<Icon name={badge.icon} /> {badge.label}
|
|
16
|
+
</span>)}
|
|
17
|
+
</span>}
|
|
25
18
|
|
|
26
19
|
<style>
|
|
27
20
|
/* Chip look comes from the .chip pattern; the read-time chip sits
|
|
@@ -1,61 +1,69 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { SitemapGroup, EcosystemEntry } from "./models";
|
|
3
3
|
import { BP_PHONE_MAX } from "../scripts/core/breakpoints";
|
|
4
|
-
|
|
5
|
-
copyright line, and the live site has no footer widget row. The sitemap
|
|
6
|
-
below the copyright line is a deliberate divergence, owner-directed
|
|
7
|
-
2026-07-30 (docs/superpowers/specs/2026-07-30-footer-sitemap-design.md).
|
|
8
|
-
The old theme credit stays dropped: this build is original code, not the
|
|
9
|
-
Adore theme. Everything below renders from the sitemap/ecosystem props
|
|
10
|
-
Base.astro passes in; SitemapGroup and EcosystemEntry live in models.ts. */
|
|
4
|
+
|
|
11
5
|
interface Props {
|
|
12
6
|
siteName: string;
|
|
13
7
|
legalHolder: string;
|
|
14
8
|
sitemap: SitemapGroup[];
|
|
15
9
|
ecosystem: EcosystemEntry[];
|
|
16
|
-
/* The entry rendered as this site itself (unlinked, bold). */
|
|
17
10
|
ecosystemSelf: string;
|
|
18
11
|
ecosystemTitle?: string;
|
|
19
12
|
}
|
|
20
|
-
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
siteName,
|
|
16
|
+
legalHolder,
|
|
17
|
+
sitemap,
|
|
18
|
+
ecosystem,
|
|
19
|
+
ecosystemSelf,
|
|
20
|
+
ecosystemTitle = "Ecosystem",
|
|
21
|
+
} = Astro.props;
|
|
22
|
+
|
|
21
23
|
const year = new Date().getFullYear();
|
|
22
24
|
---
|
|
25
|
+
|
|
23
26
|
<footer id="colophon" class="site-footer">
|
|
24
27
|
<div class="bottom-footer tint-overlay">
|
|
25
28
|
<div class="shell">
|
|
26
29
|
<div class="bottom-footer-info">
|
|
27
30
|
<div class="site-info">
|
|
28
|
-
{/*
|
|
29
|
-
the
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<slot name="site-info"
|
|
31
|
+
{/* Two lines at every width (owner call 2026-09-13): the
|
|
32
|
+
copyright over the site name, a real break rather than the
|
|
33
|
+
old pipe that retired on phones and left the wrap to do
|
|
34
|
+
the separating. The copyright's segments stay unbreakable
|
|
35
|
+
so a narrow screen wraps that line only after the year. */}
|
|
36
|
+
<slot name="site-info">
|
|
37
|
+
<span class="site-info-line">
|
|
34
38
|
<span class="site-info-seg">Copyright © {year}</span>{" "}
|
|
35
|
-
<span class="site-info-seg">{legalHolder}</span>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
<span class="site-info-seg">{legalHolder}</span>
|
|
40
|
+
</span>
|
|
41
|
+
<span class="site-info-line"><a href="/">{siteName}</a></span>
|
|
42
|
+
</slot>
|
|
39
43
|
</div>
|
|
40
44
|
</div>
|
|
41
45
|
<nav class="footer-sitemap" aria-label="Sitemap">
|
|
42
46
|
{sitemap.map((group) =>
|
|
43
47
|
group.collapsible ? (
|
|
44
|
-
<details
|
|
48
|
+
<details
|
|
49
|
+
class="footer-sitemap-group footer-sitemap-collapsible"
|
|
50
|
+
open
|
|
51
|
+
data-phone-open={group.phoneOpen ? "" : undefined}
|
|
52
|
+
>
|
|
45
53
|
<summary><h2>{group.title}</h2></summary>
|
|
46
54
|
<ul>
|
|
47
|
-
{group.links.map((link) =>
|
|
48
|
-
<
|
|
49
|
-
)
|
|
55
|
+
{group.links.map((link) => <li>
|
|
56
|
+
<a href={link.href}>{link.label}</a>
|
|
57
|
+
</li>)}
|
|
50
58
|
</ul>
|
|
51
59
|
</details>
|
|
52
60
|
) : (
|
|
53
61
|
<section class="footer-sitemap-group">
|
|
54
62
|
<h2>{group.title}</h2>
|
|
55
63
|
<ul>
|
|
56
|
-
{group.links.map((link) =>
|
|
57
|
-
<
|
|
58
|
-
)
|
|
64
|
+
{group.links.map((link) => <li>
|
|
65
|
+
<a href={link.href}>{link.label}</a>
|
|
66
|
+
</li>)}
|
|
59
67
|
</ul>
|
|
60
68
|
</section>
|
|
61
69
|
),
|
|
@@ -63,17 +71,23 @@ const year = new Date().getFullYear();
|
|
|
63
71
|
<details class="footer-sitemap-group footer-sitemap-collapsible" open>
|
|
64
72
|
<summary><h2>{ecosystemTitle}</h2></summary>
|
|
65
73
|
<ul data-ecosystem>
|
|
66
|
-
{ecosystem.map((entry) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
{ecosystem.map((entry) => {
|
|
75
|
+
/* Three states, one block each: this site, a live
|
|
76
|
+
property, a property with no href yet. */
|
|
77
|
+
const self = entry.key === ecosystemSelf;
|
|
78
|
+
const live = !self && entry.href !== null;
|
|
79
|
+
const pending = !self && !live;
|
|
80
|
+
|
|
81
|
+
return <li>
|
|
82
|
+
{self && <span class="footer-sitemap-self" aria-current="page"
|
|
83
|
+
>{entry.label}</span
|
|
84
|
+
>}{live && <a href={entry.href}
|
|
85
|
+
>{entry.label}</a
|
|
86
|
+
>}{pending && <span class="footer-sitemap-pending"
|
|
87
|
+
>{entry.label}</span
|
|
88
|
+
>}
|
|
89
|
+
</li>;
|
|
90
|
+
})}
|
|
77
91
|
</ul>
|
|
78
92
|
</details>
|
|
79
93
|
</nav>
|
|
@@ -97,12 +111,14 @@ const year = new Date().getFullYear();
|
|
|
97
111
|
|
|
98
112
|
<style>
|
|
99
113
|
.site-footer { margin-top: 50px; }
|
|
114
|
+
|
|
100
115
|
/* Tint comes from the .tint-overlay pattern (default 0.1 opacity).
|
|
101
116
|
The bottom padding adds --dock-bottom (tokens/primitives.css): a
|
|
102
117
|
site with a viewport-fixed bottom cluster sets it to the room
|
|
103
118
|
that cluster occupies, and the tinted band runs on below the last
|
|
104
119
|
link by that much, so the page scrolls far enough to clear it. */
|
|
105
120
|
.bottom-footer { padding: 15px 0 calc(20px + var(--dock-bottom, 0px)); z-index: 1; }
|
|
121
|
+
|
|
106
122
|
.bottom-footer-info {
|
|
107
123
|
display: flex;
|
|
108
124
|
flex-wrap: wrap;
|
|
@@ -111,20 +127,18 @@ const year = new Date().getFullYear();
|
|
|
111
127
|
position: relative;
|
|
112
128
|
font-size: var(--font-size-sm);
|
|
113
129
|
}
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
|
|
131
|
+
/* The two lines center under each other like the sitemap below
|
|
132
|
+
(owner report 2026-08-25). */
|
|
116
133
|
.site-info { text-align: center; }
|
|
134
|
+
.site-info-line { display: block; }
|
|
135
|
+
|
|
117
136
|
/* Underlined, unlike the sitemap links below: this one sits inside
|
|
118
|
-
running text (the copyright
|
|
137
|
+
running text (the copyright block), where the same ink at 0.8
|
|
119
138
|
opacity is not a visible link mark (WCAG 1.4.1, axe
|
|
120
|
-
link-in-text-block;
|
|
121
|
-
where the pipe retires and the line break is all that frames it). */
|
|
139
|
+
link-in-text-block; the line break is all that frames it). */
|
|
122
140
|
.site-info a { text-decoration: underline; opacity: 0.8; transition: var(--transition); color: var(--ink); }
|
|
123
141
|
.site-info-seg { white-space: nowrap; }
|
|
124
|
-
/* The pipe's separating job is done by the line break when the
|
|
125
|
-
segments wrap; below the mobile breakpoint the break is expected,
|
|
126
|
-
so the pipe retires there. */
|
|
127
|
-
@media (--bp-phone) { .site-info-sep { display: none; } }
|
|
128
142
|
.site-info a:hover { opacity: 1; }
|
|
129
143
|
|
|
130
144
|
/* Sitemap columns under the copyright line, centered like it (owner
|
|
@@ -141,25 +155,30 @@ const year = new Date().getFullYear();
|
|
|
141
155
|
border-top: 1px solid var(--hairline);
|
|
142
156
|
font-size: var(--font-size-sm);
|
|
143
157
|
}
|
|
158
|
+
|
|
144
159
|
.footer-sitemap-group h2 {
|
|
145
160
|
margin: 0 0 8px;
|
|
146
161
|
font-size: var(--font-size-xs);
|
|
147
162
|
font-weight: 700;
|
|
148
163
|
text-transform: uppercase;
|
|
149
164
|
}
|
|
165
|
+
|
|
150
166
|
.footer-sitemap-group ul {
|
|
151
167
|
list-style: none;
|
|
152
168
|
margin: 0;
|
|
153
169
|
padding: 0;
|
|
154
170
|
}
|
|
155
171
|
.footer-sitemap-group li { margin: 4px 0; }
|
|
172
|
+
|
|
156
173
|
.footer-sitemap a {
|
|
157
174
|
text-decoration: none;
|
|
158
175
|
opacity: 0.8;
|
|
159
176
|
transition: var(--transition);
|
|
160
177
|
}
|
|
178
|
+
|
|
161
179
|
.footer-sitemap a:hover,
|
|
162
180
|
.footer-sitemap a:focus { opacity: 1; text-decoration: underline; }
|
|
181
|
+
|
|
163
182
|
/* The monochrome theme makes a color marker invisible (text already IS
|
|
164
183
|
the primary color), so "you are here" is weight, not hue. */
|
|
165
184
|
.footer-sitemap-self { font-weight: 700; }
|
|
@@ -186,12 +205,14 @@ const year = new Date().getFullYear();
|
|
|
186
205
|
gap: 14px;
|
|
187
206
|
}
|
|
188
207
|
.footer-sitemap-group { text-align: center; width: 100%; }
|
|
208
|
+
|
|
189
209
|
.footer-sitemap-collapsible summary {
|
|
190
210
|
display: block;
|
|
191
211
|
position: relative;
|
|
192
212
|
cursor: pointer;
|
|
193
213
|
pointer-events: auto;
|
|
194
214
|
}
|
|
215
|
+
|
|
195
216
|
.footer-sitemap-collapsible summary::before {
|
|
196
217
|
content: "+";
|
|
197
218
|
position: absolute;
|
|
@@ -208,6 +229,7 @@ const year = new Date().getFullYear();
|
|
|
208
229
|
}
|
|
209
230
|
.footer-sitemap-collapsible[open] summary::before { content: "\2212"; }
|
|
210
231
|
.footer-sitemap-collapsible:not([open]) summary h2 { margin-bottom: 0; }
|
|
232
|
+
|
|
211
233
|
/* Keyboard focus is the site-wide ring (base/reset.css). */
|
|
212
234
|
}
|
|
213
235
|
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { iconMarkup, type IconName } from "../scripts/core/icons";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
name: IconName;
|
|
6
|
+
size?: number | string;
|
|
7
|
+
strokeWidth?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { name, size, strokeWidth, class: className } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<Fragment
|
|
15
|
+
set:html={iconMarkup(name, { size, strokeWidth, class: className })}
|
|
16
|
+
/>
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
interface Props extends Record<string, unknown> { href: string; w: number; h: number; caption?: string; class?: string }
|
|
2
|
+
interface Props extends Record<string, unknown> {
|
|
3
|
+
href: string;
|
|
4
|
+
w: number;
|
|
5
|
+
h: number;
|
|
6
|
+
caption?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
11
10
|
const { href, w, h, caption = "", class: className, ...rest } = Astro.props;
|
|
12
11
|
---
|
|
13
|
-
|
|
12
|
+
|
|
13
|
+
<a
|
|
14
|
+
class:list={["lightbox-link", className]}
|
|
15
|
+
href={href}
|
|
16
|
+
data-lb-w={w}
|
|
17
|
+
data-lb-h={h}
|
|
18
|
+
data-lb-caption={caption}
|
|
19
|
+
aria-label={`View full-size image${caption ? `: ${caption}` : ""}`}
|
|
20
|
+
{...rest}
|
|
21
|
+
><slot
|
|
22
|
+
/></a>
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
---
|
|
2
|
-
/* One list widget for the sidebar's three lists (step 6, 2026-08-30):
|
|
3
|
-
a Widget holding a link list, counts rendered when present. Replaces
|
|
4
|
-
RecentPosts, CategoriesWidget, and ArchivesWidget; the sidebar builds
|
|
5
|
-
the items via src/lib/view-models.ts. The class prop rides down to
|
|
6
|
-
Widget's root, so this file has no class:list of its own. */
|
|
7
2
|
import Widget from "./Widget.astro";
|
|
8
3
|
import type { LinkListItem } from "./models";
|
|
9
4
|
|
|
10
|
-
interface Props {
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string;
|
|
7
|
+
items: LinkListItem[];
|
|
8
|
+
headingLevel?: 2 | 3 | 4;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
11
12
|
const { title, items, headingLevel = 2, class: className } = Astro.props;
|
|
12
13
|
---
|
|
14
|
+
|
|
13
15
|
<Widget title={title} headingLevel={headingLevel} class={className}>
|
|
14
16
|
<ul>
|
|
15
|
-
{items.map((i) =>
|
|
16
|
-
<
|
|
17
|
-
)
|
|
17
|
+
{items.map((i) => <li>
|
|
18
|
+
<a href={i.href}>{i.label}</a>{i.count !== undefined && ` (${i.count})`}
|
|
19
|
+
</li>)}
|
|
18
20
|
</ul>
|
|
19
21
|
</Widget>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
/* Pagination matching the live WordPress markup: nav.navigation.pagination
|
|
3
|
-
with .nav-links containing .page-numbers boxes and Prev/Next arrows. */
|
|
4
2
|
import { archivePagePath } from "../lib/paginate";
|
|
3
|
+
import Icon from "./Icon.astro";
|
|
4
|
+
|
|
5
5
|
interface Props {
|
|
6
6
|
current: number;
|
|
7
7
|
total: number;
|
|
@@ -9,34 +9,54 @@ interface Props {
|
|
|
9
9
|
class?: string;
|
|
10
10
|
labels?: { heading?: string; prev?: string; next?: string };
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
const
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
current,
|
|
15
|
+
total,
|
|
16
|
+
base = "",
|
|
17
|
+
class: className,
|
|
18
|
+
labels: labelsOverride = {},
|
|
19
|
+
} = Astro.props;
|
|
20
|
+
|
|
21
|
+
const labels = {
|
|
22
|
+
heading: "Posts pagination",
|
|
23
|
+
prev: "Previous",
|
|
24
|
+
next: "Next",
|
|
25
|
+
...labelsOverride,
|
|
26
|
+
};
|
|
27
|
+
|
|
14
28
|
const pageHref = (n: number) => archivePagePath(n, base);
|
|
15
29
|
---
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)}
|
|
30
|
+
|
|
31
|
+
{total > 1 && <nav
|
|
32
|
+
class:list={["navigation", "pagination", className]}
|
|
33
|
+
aria-label={labels.heading}
|
|
34
|
+
>
|
|
35
|
+
<h2 class="screen-reader-text">{labels.heading}</h2>
|
|
36
|
+
<div class="nav-links">
|
|
37
|
+
{current > 1 && <a
|
|
38
|
+
class="prev page-numbers press-box"
|
|
39
|
+
href={pageHref(current - 1)}
|
|
40
|
+
>
|
|
41
|
+
<Icon name="arrow-left" size={14} />
|
|
42
|
+
<span class="screen-reader-text">{labels.prev}</span>
|
|
43
|
+
</a>}
|
|
44
|
+
{Array.from({ length: total }, (_, i) => i + 1).map((n) =>
|
|
45
|
+
n === current ? (
|
|
46
|
+
<span aria-current="page" class="page-numbers current">{n}</span>
|
|
47
|
+
) : (
|
|
48
|
+
<a class="page-numbers press-box" href={pageHref(n)}>{n}</a>
|
|
49
|
+
),
|
|
50
|
+
)}
|
|
51
|
+
{current < total && <a
|
|
52
|
+
class="next page-numbers press-box"
|
|
53
|
+
href={pageHref(current + 1)}
|
|
54
|
+
>
|
|
55
|
+
<Icon name="arrow-right" size={14} />
|
|
56
|
+
<span class="screen-reader-text">{labels.next}</span>
|
|
57
|
+
</a>}
|
|
58
|
+
</div>
|
|
59
|
+
</nav>}
|
|
40
60
|
|
|
41
61
|
<style>
|
|
42
62
|
.nav-links {
|
|
@@ -45,6 +65,7 @@ const pageHref = (n: number) => archivePagePath(n, base);
|
|
|
45
65
|
margin: 10px 0 20px;
|
|
46
66
|
justify-content: center;
|
|
47
67
|
}
|
|
68
|
+
|
|
48
69
|
.nav-links a,
|
|
49
70
|
.nav-links span {
|
|
50
71
|
text-decoration: none;
|
|
@@ -57,11 +78,13 @@ const pageHref = (n: number) => archivePagePath(n, base);
|
|
|
57
78
|
color: var(--ink);
|
|
58
79
|
border: 1px solid var(--ink);
|
|
59
80
|
}
|
|
81
|
+
|
|
60
82
|
.nav-links a:hover,
|
|
61
83
|
.nav-links a:focus {
|
|
62
84
|
color: var(--ink);
|
|
63
85
|
border-color: var(--rule);
|
|
64
86
|
}
|
|
87
|
+
|
|
65
88
|
/* Press feedback comes from .press-box on the links; the shared rule
|
|
66
89
|
above keeps this site's 1px cell border over the pattern's 2px. */
|
|
67
90
|
.nav-links span.current {
|