@half-built/astro 0.4.2 → 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.
- package/README.md +50 -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 +57 -29
- 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 +15 -0
- 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 +14 -0
- 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
|
|
|
@@ -24,6 +55,23 @@ wider preview concept (the blog's SHOW_DRAFTS builds, for example)
|
|
|
24
55
|
passes its own gate through the `shown` prop; the component reads no
|
|
25
56
|
consumer config itself.
|
|
26
57
|
|
|
58
|
+
## WhenPublished and PostLink
|
|
59
|
+
|
|
60
|
+
`content/WhenPublished.astro` renders its children only when the post
|
|
61
|
+
at `slug` is visible, so a published post can tease one still in
|
|
62
|
+
draft and the passage appears on its own when the target ships.
|
|
63
|
+
`content/PostLink.astro` links to a post by slug and resolves the
|
|
64
|
+
href at build time through `postPath`, so a re-dated post does not
|
|
65
|
+
strand the links pointing at it. Both take the consumer's collection
|
|
66
|
+
as the `posts` prop (the full collection, drafts included, so an
|
|
67
|
+
unknown slug can throw instead of hiding as "still a draft");
|
|
68
|
+
`WhenPublished` also takes the consumer's draft gate as `showDrafts`,
|
|
69
|
+
and `PostLink` accepts an optional `tip` carried as `data-tooltip` for
|
|
70
|
+
the link-tip island. A consumer wraps each in a one-line site
|
|
71
|
+
component that injects `getCollection` and its own gate, the way the
|
|
72
|
+
blog does. The resolvers live in `lib/drafts.ts` for consumers that
|
|
73
|
+
want the logic without the components.
|
|
74
|
+
|
|
27
75
|
## Live code colors
|
|
28
76
|
|
|
29
77
|
`shiki/code-theme` bakes its amber values into every highlighted span
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@half-built/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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,25 +1,28 @@
|
|
|
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">
|
|
@@ -30,32 +33,38 @@ const year = new Date().getFullYear();
|
|
|
30
33
|
which the line break replaces below the mobile breakpoint
|
|
31
34
|
(owner call 2026-08-20). Desktop and tablet render the
|
|
32
35
|
one line exactly as before. */}
|
|
33
|
-
<slot name="site-info"
|
|
36
|
+
<slot name="site-info"
|
|
37
|
+
><span>
|
|
34
38
|
<span class="site-info-seg">Copyright © {year}</span>{" "}
|
|
35
39
|
<span class="site-info-seg">{legalHolder}</span>{" "}
|
|
36
40
|
<span class="site-info-sep" aria-hidden="true">|</span>{" "}
|
|
37
41
|
<a class="site-info-seg" href="/">{siteName}</a>
|
|
38
|
-
</span></slot
|
|
42
|
+
</span></slot
|
|
43
|
+
>
|
|
39
44
|
</div>
|
|
40
45
|
</div>
|
|
41
46
|
<nav class="footer-sitemap" aria-label="Sitemap">
|
|
42
47
|
{sitemap.map((group) =>
|
|
43
48
|
group.collapsible ? (
|
|
44
|
-
<details
|
|
49
|
+
<details
|
|
50
|
+
class="footer-sitemap-group footer-sitemap-collapsible"
|
|
51
|
+
open
|
|
52
|
+
data-phone-open={group.phoneOpen ? "" : undefined}
|
|
53
|
+
>
|
|
45
54
|
<summary><h2>{group.title}</h2></summary>
|
|
46
55
|
<ul>
|
|
47
|
-
{group.links.map((link) =>
|
|
48
|
-
<
|
|
49
|
-
)
|
|
56
|
+
{group.links.map((link) => <li>
|
|
57
|
+
<a href={link.href}>{link.label}</a>
|
|
58
|
+
</li>)}
|
|
50
59
|
</ul>
|
|
51
60
|
</details>
|
|
52
61
|
) : (
|
|
53
62
|
<section class="footer-sitemap-group">
|
|
54
63
|
<h2>{group.title}</h2>
|
|
55
64
|
<ul>
|
|
56
|
-
{group.links.map((link) =>
|
|
57
|
-
<
|
|
58
|
-
)
|
|
65
|
+
{group.links.map((link) => <li>
|
|
66
|
+
<a href={link.href}>{link.label}</a>
|
|
67
|
+
</li>)}
|
|
59
68
|
</ul>
|
|
60
69
|
</section>
|
|
61
70
|
),
|
|
@@ -63,17 +72,23 @@ const year = new Date().getFullYear();
|
|
|
63
72
|
<details class="footer-sitemap-group footer-sitemap-collapsible" open>
|
|
64
73
|
<summary><h2>{ecosystemTitle}</h2></summary>
|
|
65
74
|
<ul data-ecosystem>
|
|
66
|
-
{ecosystem.map((entry) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
{ecosystem.map((entry) => {
|
|
76
|
+
/* Three states, one block each: this site, a live
|
|
77
|
+
property, a property with no href yet. */
|
|
78
|
+
const self = entry.key === ecosystemSelf;
|
|
79
|
+
const live = !self && entry.href !== null;
|
|
80
|
+
const pending = !self && !live;
|
|
81
|
+
|
|
82
|
+
return <li>
|
|
83
|
+
{self && <span class="footer-sitemap-self" aria-current="page"
|
|
84
|
+
>{entry.label}</span
|
|
85
|
+
>}{live && <a href={entry.href}
|
|
86
|
+
>{entry.label}</a
|
|
87
|
+
>}{pending && <span class="footer-sitemap-pending"
|
|
88
|
+
>{entry.label}</span
|
|
89
|
+
>}
|
|
90
|
+
</li>;
|
|
91
|
+
})}
|
|
77
92
|
</ul>
|
|
78
93
|
</details>
|
|
79
94
|
</nav>
|
|
@@ -97,12 +112,14 @@ const year = new Date().getFullYear();
|
|
|
97
112
|
|
|
98
113
|
<style>
|
|
99
114
|
.site-footer { margin-top: 50px; }
|
|
115
|
+
|
|
100
116
|
/* Tint comes from the .tint-overlay pattern (default 0.1 opacity).
|
|
101
117
|
The bottom padding adds --dock-bottom (tokens/primitives.css): a
|
|
102
118
|
site with a viewport-fixed bottom cluster sets it to the room
|
|
103
119
|
that cluster occupies, and the tinted band runs on below the last
|
|
104
120
|
link by that much, so the page scrolls far enough to clear it. */
|
|
105
121
|
.bottom-footer { padding: 15px 0 calc(20px + var(--dock-bottom, 0px)); z-index: 1; }
|
|
122
|
+
|
|
106
123
|
.bottom-footer-info {
|
|
107
124
|
display: flex;
|
|
108
125
|
flex-wrap: wrap;
|
|
@@ -111,9 +128,11 @@ const year = new Date().getFullYear();
|
|
|
111
128
|
position: relative;
|
|
112
129
|
font-size: var(--font-size-sm);
|
|
113
130
|
}
|
|
131
|
+
|
|
114
132
|
/* The wrapped phone lines center under each other like the sitemap
|
|
115
133
|
below (owner report 2026-08-25); one line on desktop, as before. */
|
|
116
134
|
.site-info { text-align: center; }
|
|
135
|
+
|
|
117
136
|
/* Underlined, unlike the sitemap links below: this one sits inside
|
|
118
137
|
running text (the copyright line), where the same ink at 0.8
|
|
119
138
|
opacity is not a visible link mark (WCAG 1.4.1, axe
|
|
@@ -121,6 +140,7 @@ const year = new Date().getFullYear();
|
|
|
121
140
|
where the pipe retires and the line break is all that frames it). */
|
|
122
141
|
.site-info a { text-decoration: underline; opacity: 0.8; transition: var(--transition); color: var(--ink); }
|
|
123
142
|
.site-info-seg { white-space: nowrap; }
|
|
143
|
+
|
|
124
144
|
/* The pipe's separating job is done by the line break when the
|
|
125
145
|
segments wrap; below the mobile breakpoint the break is expected,
|
|
126
146
|
so the pipe retires there. */
|
|
@@ -141,25 +161,30 @@ const year = new Date().getFullYear();
|
|
|
141
161
|
border-top: 1px solid var(--hairline);
|
|
142
162
|
font-size: var(--font-size-sm);
|
|
143
163
|
}
|
|
164
|
+
|
|
144
165
|
.footer-sitemap-group h2 {
|
|
145
166
|
margin: 0 0 8px;
|
|
146
167
|
font-size: var(--font-size-xs);
|
|
147
168
|
font-weight: 700;
|
|
148
169
|
text-transform: uppercase;
|
|
149
170
|
}
|
|
171
|
+
|
|
150
172
|
.footer-sitemap-group ul {
|
|
151
173
|
list-style: none;
|
|
152
174
|
margin: 0;
|
|
153
175
|
padding: 0;
|
|
154
176
|
}
|
|
155
177
|
.footer-sitemap-group li { margin: 4px 0; }
|
|
178
|
+
|
|
156
179
|
.footer-sitemap a {
|
|
157
180
|
text-decoration: none;
|
|
158
181
|
opacity: 0.8;
|
|
159
182
|
transition: var(--transition);
|
|
160
183
|
}
|
|
184
|
+
|
|
161
185
|
.footer-sitemap a:hover,
|
|
162
186
|
.footer-sitemap a:focus { opacity: 1; text-decoration: underline; }
|
|
187
|
+
|
|
163
188
|
/* The monochrome theme makes a color marker invisible (text already IS
|
|
164
189
|
the primary color), so "you are here" is weight, not hue. */
|
|
165
190
|
.footer-sitemap-self { font-weight: 700; }
|
|
@@ -186,12 +211,14 @@ const year = new Date().getFullYear();
|
|
|
186
211
|
gap: 14px;
|
|
187
212
|
}
|
|
188
213
|
.footer-sitemap-group { text-align: center; width: 100%; }
|
|
214
|
+
|
|
189
215
|
.footer-sitemap-collapsible summary {
|
|
190
216
|
display: block;
|
|
191
217
|
position: relative;
|
|
192
218
|
cursor: pointer;
|
|
193
219
|
pointer-events: auto;
|
|
194
220
|
}
|
|
221
|
+
|
|
195
222
|
.footer-sitemap-collapsible summary::before {
|
|
196
223
|
content: "+";
|
|
197
224
|
position: absolute;
|
|
@@ -208,6 +235,7 @@ const year = new Date().getFullYear();
|
|
|
208
235
|
}
|
|
209
236
|
.footer-sitemap-collapsible[open] summary::before { content: "\2212"; }
|
|
210
237
|
.footer-sitemap-collapsible:not([open]) summary h2 { margin-bottom: 0; }
|
|
238
|
+
|
|
211
239
|
/* Keyboard focus is the site-wide ring (base/reset.css). */
|
|
212
240
|
}
|
|
213
241
|
</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 {
|