@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
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
---
|
|
2
|
-
/* Day/night toggle (owner request 2026-08-25): an .icon-box button that
|
|
3
|
-
flips the site theme. Both glyphs are in the button; CSS shows the
|
|
4
|
-
current theme's (sun by day, moon by night, owner call 2026-08-26) and
|
|
5
|
-
the label names the action. State and storage live in
|
|
6
|
-
scripts/theme-toggle.ts, which mounts every .theme-toggle on the page
|
|
7
|
-
and keeps them in step; the first paint is stamped by the inline
|
|
8
|
-
script in Shell.astro's head.
|
|
9
|
-
|
|
10
|
-
Two placements: "band" sits beside the date stamp in the header's top
|
|
11
|
-
band; "phone" sits beside the search icon in the bottom band and shows
|
|
12
|
-
only under 768px, where the top band is hidden (owner call 2026-08-20),
|
|
13
|
-
so phones keep a toggle (owner request 2026-08-26). */
|
|
14
2
|
import { ICON_SUN, ICON_MOON } from "../scripts/core/icons";
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
placement?: "band" | "phone";
|
|
6
|
+
class?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
placement = "band",
|
|
12
|
+
class: className,
|
|
13
|
+
label = "Switch to dark mode",
|
|
14
|
+
} = Astro.props;
|
|
17
15
|
---
|
|
18
|
-
|
|
16
|
+
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
class:list={[
|
|
20
|
+
"theme-toggle",
|
|
21
|
+
"icon-box",
|
|
22
|
+
placement === "phone" && "theme-toggle-phone",
|
|
23
|
+
className,
|
|
24
|
+
]}
|
|
25
|
+
aria-label={label}
|
|
26
|
+
aria-pressed="false"
|
|
27
|
+
>
|
|
19
28
|
<span class="theme-toggle-moon"><Fragment set:html={ICON_MOON} /></span>
|
|
20
29
|
<span class="theme-toggle-sun"><Fragment set:html={ICON_SUN} /></span>
|
|
21
30
|
</button>
|
|
@@ -23,18 +32,22 @@ const { placement = "band", class: className, label = "Switch to dark mode" } =
|
|
|
23
32
|
<style>
|
|
24
33
|
.theme-toggle { font-size: 16px; }
|
|
25
34
|
.theme-toggle span { display: block; line-height: 0; }
|
|
35
|
+
|
|
26
36
|
/* The glyphs keep their currentColor stroke and take a fill: the moon
|
|
27
37
|
in the second accent, the sun's disc in the first (owner call
|
|
28
38
|
2026-08-26). :global because the svg arrives through set:html. */
|
|
29
39
|
.theme-toggle-moon :global(path) { fill: var(--accent-2); }
|
|
30
40
|
.theme-toggle-sun :global(circle) { fill: var(--accent-1); }
|
|
41
|
+
|
|
31
42
|
/* Show the glyph of the current theme. Both selectors carry the
|
|
32
43
|
parent class so they outrank the span rule above. */
|
|
33
44
|
.theme-toggle .theme-toggle-moon { display: none; }
|
|
34
45
|
:global(:root[data-theme="dark"]) .theme-toggle .theme-toggle-moon { display: block; }
|
|
35
46
|
:global(:root[data-theme="dark"]) .theme-toggle .theme-toggle-sun { display: none; }
|
|
47
|
+
|
|
36
48
|
/* The phone placement exists only where the top band does not. */
|
|
37
49
|
.theme-toggle-phone { display: none; }
|
|
50
|
+
|
|
38
51
|
@media (--bp-phone) {
|
|
39
52
|
.theme-toggle-phone { display: flex; }
|
|
40
53
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
interface Props { class?: string }
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
7
6
|
const { class: className } = Astro.props;
|
|
8
7
|
---
|
|
8
|
+
|
|
9
9
|
<div class:list={["two-column", className]}>
|
|
10
10
|
<main class="site-main"><slot /></main>
|
|
11
11
|
<slot name="aside" />
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
headingLevel?: 2 | 3 | 4;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
7
8
|
const { title, headingLevel = 2, class: className } = Astro.props;
|
|
8
|
-
/* Annotated, not cast: an `as` cast in the frontmatter can make the Astro
|
|
9
|
-
compiler drop the Props type (see PostCard.astro). */
|
|
10
9
|
const Heading: "h2" | "h3" | "h4" = `h${headingLevel}`;
|
|
11
10
|
---
|
|
11
|
+
|
|
12
12
|
<section class:list={["widget", className]}>
|
|
13
13
|
<Heading class="widget-title">{title}</Heading>
|
|
14
14
|
<div class="widget-body"><slot /></div>
|
|
@@ -16,6 +16,7 @@ const Heading: "h2" | "h3" | "h4" = `h${headingLevel}`;
|
|
|
16
16
|
|
|
17
17
|
<style>
|
|
18
18
|
.widget { margin: 0 0 2em; }
|
|
19
|
+
|
|
19
20
|
/* Owner ruling 2026-07-26 (diverges from live's larger headings):
|
|
20
21
|
widget headers share the Subscribe panel's title scale. */
|
|
21
22
|
.widget-title {
|
|
@@ -25,6 +26,7 @@ const Heading: "h2" | "h3" | "h4" = `h${headingLevel}`;
|
|
|
25
26
|
font-size: var(--font-size-base);
|
|
26
27
|
}
|
|
27
28
|
.widget-body :global(ul) { list-style: none; padding: 0; margin: 0; }
|
|
29
|
+
|
|
28
30
|
.widget-body :global(li) {
|
|
29
31
|
margin-bottom: 7px;
|
|
30
32
|
padding-bottom: 7px;
|
|
@@ -32,6 +34,7 @@ const Heading: "h2" | "h3" | "h4" = `h${headingLevel}`;
|
|
|
32
34
|
border-bottom: 1px solid var(--divider-faint);
|
|
33
35
|
}
|
|
34
36
|
.widget-body :global(li:last-child) { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
|
|
37
|
+
|
|
35
38
|
.widget-body :global(a) {
|
|
36
39
|
color: var(--ink);
|
|
37
40
|
transition: var(--transition);
|
|
@@ -1,24 +1,57 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ImageMetadata } from "astro";
|
|
3
3
|
import CornerBadges from "../CornerBadges.astro";
|
|
4
|
+
import type { Badge } from "../../scripts/core/badges";
|
|
4
5
|
import SmartImage from "../SmartImage.astro";
|
|
5
6
|
import LightboxLink from "../LightboxLink.astro";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
interface Props {
|
|
9
|
+
src: ImageMetadata;
|
|
10
|
+
alt?: string;
|
|
11
|
+
caption?: string;
|
|
12
|
+
frameless?: boolean;
|
|
13
|
+
narrow?: boolean;
|
|
14
|
+
badges?: Badge[];
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
src,
|
|
20
|
+
alt = "",
|
|
21
|
+
caption,
|
|
22
|
+
frameless = false,
|
|
23
|
+
narrow = false,
|
|
24
|
+
badges = [],
|
|
25
|
+
class: className,
|
|
26
|
+
} = Astro.props;
|
|
16
27
|
---
|
|
28
|
+
|
|
17
29
|
{/* Image branching and lb-attrs live in the primitives (step 5). */}
|
|
18
|
-
<figure
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
<figure
|
|
31
|
+
class:list={[
|
|
32
|
+
"blog-image",
|
|
33
|
+
frameless && "blog-image-frameless",
|
|
34
|
+
narrow && "blog-image-narrow",
|
|
35
|
+
className,
|
|
36
|
+
]}
|
|
37
|
+
>
|
|
38
|
+
<LightboxLink
|
|
39
|
+
href={src.src}
|
|
40
|
+
w={src.width}
|
|
41
|
+
h={src.height}
|
|
42
|
+
caption={caption ?? alt}
|
|
43
|
+
>
|
|
44
|
+
<SmartImage
|
|
45
|
+
src={src}
|
|
46
|
+
alt={alt}
|
|
47
|
+
widths={[495, 990, Math.min(src.width, 1980)]}
|
|
48
|
+
sizes={
|
|
49
|
+
narrow
|
|
50
|
+
? "(max-width: 1024px) 50vw, 495px"
|
|
51
|
+
: "(max-width: 1024px) 100vw, 990px"
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
<CornerBadges badges={badges} />
|
|
22
55
|
</LightboxLink>
|
|
23
56
|
{caption && <figcaption class="caption">{caption}</figcaption>}
|
|
24
57
|
</figure>
|
|
@@ -26,6 +59,7 @@ const { src, alt = "", caption, frameless = false, narrow = false, genai = false
|
|
|
26
59
|
<style>
|
|
27
60
|
.blog-image { margin: 20px 0; }
|
|
28
61
|
.blog-image-frameless :global(img) { border: 0; }
|
|
62
|
+
|
|
29
63
|
/* Images narrower than the column center, matching the centered
|
|
30
64
|
captions (full-width images are unaffected). The link hugs the image
|
|
31
65
|
(fit-content, centered) rather than spanning the column, so a corner
|
|
@@ -33,6 +67,7 @@ const { src, alt = "", caption, frameless = false, narrow = false, genai = false
|
|
|
33
67
|
.blog-image :global(.lightbox-link) { width: fit-content; max-width: 100%; margin-inline: auto; }
|
|
34
68
|
.blog-image :global(img) { max-width: 100%; height: auto; display: block; margin-inline: auto; }
|
|
35
69
|
.blog-image-narrow :global(.lightbox-link) { max-width: min(495px, 100%); }
|
|
70
|
+
|
|
36
71
|
/* Type from the .caption pattern. Fidelity pass (handoff 5.5): live
|
|
37
72
|
figcaptions are centered. */
|
|
38
73
|
figcaption { margin-top: 5px; text-align: center; }
|
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
/* The house button (owner request 2026-08-26): one component over the
|
|
3
|
-
press-box patterns so every button on the site, and later every site
|
|
4
|
-
on the library, draws the same three looks.
|
|
5
|
-
|
|
6
|
-
variant: "plain" is the ink press box (nav links, pagination); "shaded"
|
|
7
|
-
is the same box on the nav's current-item shade; "accent-1"
|
|
8
|
-
wears the brand amber and "accent-2" the cyan, both in the callout
|
|
9
|
-
combination (accent line and ink on a faint tint, filling solid while
|
|
10
|
-
held). size: "md" is the nav-box size, "lg" the call-to-action size.
|
|
11
|
-
With href it renders an anchor, otherwise a button (type "button"
|
|
12
|
-
unless told otherwise). Put an icon first in the slot and it gets the
|
|
13
|
-
gap; the text is the slot. */
|
|
14
2
|
interface Props {
|
|
15
3
|
variant?: "plain" | "shaded" | "accent-1" | "accent-2";
|
|
16
4
|
size?: "md" | "lg";
|
|
@@ -18,7 +6,15 @@ interface Props {
|
|
|
18
6
|
type?: "button" | "submit";
|
|
19
7
|
class?: string;
|
|
20
8
|
}
|
|
21
|
-
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
variant = "plain",
|
|
12
|
+
size = "md",
|
|
13
|
+
href,
|
|
14
|
+
type = "button",
|
|
15
|
+
class: className,
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
|
|
22
18
|
const classes = [
|
|
23
19
|
"button",
|
|
24
20
|
"press-box",
|
|
@@ -29,6 +25,7 @@ const classes = [
|
|
|
29
25
|
`button-${size}`,
|
|
30
26
|
];
|
|
31
27
|
---
|
|
28
|
+
|
|
32
29
|
{href ? (
|
|
33
30
|
<a class:list={[...classes, className]} href={href}><slot /></a>
|
|
34
31
|
) : (
|
|
@@ -41,16 +38,19 @@ const classes = [
|
|
|
41
38
|
font-weight: 700;
|
|
42
39
|
text-align: center;
|
|
43
40
|
}
|
|
41
|
+
|
|
44
42
|
.button-md {
|
|
45
43
|
padding: 7px 15px;
|
|
46
44
|
font-size: var(--font-size-sm);
|
|
47
45
|
text-transform: uppercase;
|
|
48
46
|
}
|
|
47
|
+
|
|
49
48
|
.button-lg {
|
|
50
49
|
padding: 10px 24px;
|
|
51
50
|
font-size: var(--font-size-md);
|
|
52
51
|
letter-spacing: 0.08em;
|
|
53
52
|
}
|
|
53
|
+
|
|
54
54
|
/* A leading icon sits on the text's baseline with a gap; the svg comes
|
|
55
55
|
through the slot, hence :global. */
|
|
56
56
|
.button :global(svg:first-child) { vertical-align: -0.15em; margin-right: 8px; }
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
---
|
|
2
|
-
/* Admonition panel (Confluence-style info/warning/error blocks), owner
|
|
3
|
-
request 2026-08-21, rendered in the house line system: a 2px box in the
|
|
4
|
-
type's line color, a low-opacity tint of the same color, and a label
|
|
5
|
-
straddling the top rule the way the theme's date boxes do. Inks stay in
|
|
6
|
-
the house families (graphite, amber, cyan, red): note and info read
|
|
7
|
-
graphite and the type is told by the label text, tip and warning take
|
|
8
|
-
the first accent, success the second, error the red. */
|
|
9
2
|
interface Props {
|
|
10
3
|
type?: "note" | "info" | "tip" | "success" | "warning" | "error";
|
|
11
4
|
title?: string;
|
|
12
5
|
class?: string;
|
|
13
6
|
}
|
|
7
|
+
|
|
14
8
|
const { type = "note", title, class: className } = Astro.props;
|
|
15
9
|
const label = title ?? type;
|
|
16
10
|
---
|
|
11
|
+
|
|
17
12
|
{/* role="note", not <aside>: aside is a landmark, and html-validate's
|
|
18
13
|
unique-landmark rule (rightly) demands unique names, which repeated
|
|
19
14
|
callouts on one page cannot provide. */}
|
|
20
|
-
<div
|
|
15
|
+
<div
|
|
16
|
+
class:list={[
|
|
17
|
+
"callout",
|
|
18
|
+
`callout-${type}`,
|
|
19
|
+
"rule-box",
|
|
20
|
+
"tint-overlay",
|
|
21
|
+
className,
|
|
22
|
+
]}
|
|
23
|
+
role="note"
|
|
24
|
+
>
|
|
21
25
|
<strong class="callout-label boxed-label">{label}</strong>
|
|
22
26
|
<div class="callout-body"><slot /></div>
|
|
23
27
|
</div>
|
|
@@ -33,10 +37,12 @@ const label = title ?? type;
|
|
|
33
37
|
margin: 30px 0;
|
|
34
38
|
}
|
|
35
39
|
.callout::before { pointer-events: none; }
|
|
40
|
+
|
|
36
41
|
.callout-label {
|
|
37
42
|
position: absolute;
|
|
38
43
|
top: 0;
|
|
39
44
|
left: 20px;
|
|
45
|
+
|
|
40
46
|
/* Center the label on the 2px top rule, not on the padding-box edge. */
|
|
41
47
|
transform: translateY(calc(-50% - 2px));
|
|
42
48
|
padding: 4px 10px;
|
|
@@ -57,17 +63,20 @@ const label = title ?? type;
|
|
|
57
63
|
--callout-line: var(--rule);
|
|
58
64
|
--callout-ink: var(--ink);
|
|
59
65
|
}
|
|
66
|
+
|
|
60
67
|
.callout-tip,
|
|
61
68
|
.callout-warning {
|
|
62
69
|
--callout-line: var(--accent-1);
|
|
63
70
|
--callout-ink: var(--accent-1-ink);
|
|
64
71
|
}
|
|
72
|
+
|
|
65
73
|
/* Success takes the second accent (owner call 2026-08-26): cyan is the
|
|
66
74
|
"it worked" temperature, and it keeps the amber for tip and warning. */
|
|
67
75
|
.callout-success {
|
|
68
76
|
--callout-line: var(--accent-2);
|
|
69
77
|
--callout-ink: var(--accent-2-ink);
|
|
70
78
|
}
|
|
79
|
+
|
|
71
80
|
.callout-error {
|
|
72
81
|
--callout-line: var(--error-ink);
|
|
73
82
|
--callout-ink: var(--error-ink);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
interface Props {
|
|
2
|
+
interface Props {
|
|
3
|
+
filename?: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
const { filename, class: className } = Astro.props;
|
|
4
8
|
---
|
|
9
|
+
|
|
5
10
|
<div class:list={["code-block", className]} data-code-filename={filename}>
|
|
6
11
|
<slot />
|
|
7
12
|
</div>
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
interface Props {
|
|
3
|
+
title?: string;
|
|
4
|
+
shown?: boolean;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
title,
|
|
10
|
+
shown = import.meta.env.MODE === "development",
|
|
11
|
+
class: className,
|
|
12
|
+
} = Astro.props;
|
|
13
13
|
|
|
14
|
-
/* The label always reads "Editor's Note", with the title after a colon
|
|
15
|
-
when one is given (owner call 2026-08-27). */
|
|
16
|
-
interface Props { title?: string; shown?: boolean; class?: string }
|
|
17
|
-
const { title, shown = import.meta.env.MODE === "development", class: className } = Astro.props;
|
|
18
14
|
const label = title ? `Editor's Note: ${title}` : "Editor's Note";
|
|
19
15
|
---
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)}
|
|
16
|
+
|
|
17
|
+
{shown && <div class:list={["editor-note", "rule-box", className]} role="note">
|
|
18
|
+
<strong class="editor-note-label boxed-label">{label}</strong>
|
|
19
|
+
<div class="editor-note-body"><slot /></div>
|
|
20
|
+
</div>}
|
|
26
21
|
|
|
27
22
|
<style>
|
|
28
23
|
/* Box from .rule-box; the dashed amber stroke and the margin are here. */
|
|
@@ -32,10 +27,12 @@ const label = title ? `Editor's Note: ${title}` : "Editor's Note";
|
|
|
32
27
|
border-color: var(--accent-1);
|
|
33
28
|
margin: 30px 0;
|
|
34
29
|
}
|
|
30
|
+
|
|
35
31
|
.editor-note-label {
|
|
36
32
|
position: absolute;
|
|
37
33
|
top: 0;
|
|
38
34
|
left: 20px;
|
|
35
|
+
|
|
39
36
|
/* Center the label on the 2px top rule, not on the padding-box edge. */
|
|
40
37
|
transform: translateY(calc(-50% - 2px));
|
|
41
38
|
padding: 4px 10px;
|
|
@@ -47,6 +44,7 @@ const label = title ? `Editor's Note: ${title}` : "Editor's Note";
|
|
|
47
44
|
color: var(--accent-1-ink);
|
|
48
45
|
border-color: var(--accent-1);
|
|
49
46
|
}
|
|
47
|
+
|
|
50
48
|
/* Reads as an annotation, not a paragraph: a step smaller than prose. */
|
|
51
49
|
.editor-note-body {
|
|
52
50
|
position: relative;
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
interface Props {
|
|
3
|
+
columns?: number;
|
|
4
|
+
caption?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
6
8
|
const { columns = 3, caption, class: className } = Astro.props;
|
|
7
9
|
const hasCaptionSlot = Astro.slots.has("caption");
|
|
8
10
|
---
|
|
11
|
+
|
|
9
12
|
{/* Frame comes from the .bracket-frame pattern, same enclosure as the
|
|
10
13
|
article itself (owner request 2026-07-29). */}
|
|
11
14
|
<figure class:list={["gallery-figure", className]}>
|
|
12
|
-
<div
|
|
15
|
+
<div
|
|
16
|
+
class:list={["gallery-plates", "bracket-frame", `gallery-cols-${columns}`]}
|
|
17
|
+
style={`--gallery-cols:${columns}`}
|
|
18
|
+
>
|
|
13
19
|
<slot />
|
|
14
20
|
</div>
|
|
15
21
|
{hasCaptionSlot ? (
|
|
@@ -23,16 +29,21 @@ const hasCaptionSlot = Astro.slots.has("caption");
|
|
|
23
29
|
/* The figure wrapper exists so a gallery-level caption reads as part of
|
|
24
30
|
the gallery (owner request 2026-07-30); outer spacing lives here. */
|
|
25
31
|
.gallery-figure { margin: 20px 0; }
|
|
32
|
+
|
|
26
33
|
.gallery-plates {
|
|
27
34
|
display: grid;
|
|
28
35
|
grid-template-columns: repeat(var(--gallery-cols), 1fr);
|
|
29
36
|
gap: 12px;
|
|
30
37
|
}
|
|
38
|
+
|
|
31
39
|
@media (--bp-phone) { .gallery-plates { grid-template-columns: repeat(2, 1fr); } }
|
|
40
|
+
|
|
32
41
|
@media (max-width: 480px) { .gallery-plates { grid-template-columns: 1fr; } }
|
|
42
|
+
|
|
33
43
|
/* Type from the .caption pattern; centered under the plates like
|
|
34
44
|
BlogImage's. */
|
|
35
45
|
figcaption { margin-top: 5px; text-align: center; }
|
|
46
|
+
|
|
36
47
|
/* Orphan rule (owner request 2026-07-29): a lone plate on the last row
|
|
37
48
|
centers instead of hugging the left column. The :global() escape is
|
|
38
49
|
needed because plates are GalleryImage children, outside this
|
|
@@ -40,12 +51,14 @@ const hasCaptionSlot = Astro.slots.has("caption");
|
|
|
40
51
|
math differs per count; columns other than 2 and 3 get no rule. */
|
|
41
52
|
@media (--bp-phone-up) {
|
|
42
53
|
.gallery-cols-3 > :global(:nth-child(3n + 1):last-child) { grid-column: 2; }
|
|
54
|
+
|
|
43
55
|
.gallery-cols-2 > :global(:nth-child(odd):last-child) {
|
|
44
56
|
grid-column: 1 / -1;
|
|
45
57
|
justify-self: center;
|
|
46
58
|
width: calc(50% - 6px);
|
|
47
59
|
}
|
|
48
60
|
}
|
|
61
|
+
|
|
49
62
|
/* Below 768px every gallery is two columns, whatever it asked for. */
|
|
50
63
|
@media (min-width: 481px) and (--bp-phone) {
|
|
51
64
|
.gallery-plates > :global(:nth-child(odd):last-child) {
|
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ImageMetadata } from "astro";
|
|
3
3
|
import CornerBadges from "../CornerBadges.astro";
|
|
4
|
+
import type { Badge } from "../../scripts/core/badges";
|
|
4
5
|
import SmartImage from "../SmartImage.astro";
|
|
5
6
|
import LightboxLink from "../LightboxLink.astro";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
interface Props {
|
|
9
|
+
src: ImageMetadata;
|
|
10
|
+
alt?: string;
|
|
11
|
+
caption?: string;
|
|
12
|
+
badges?: Badge[];
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { src, alt = "", caption, badges = [], class: className } = Astro.props;
|
|
11
17
|
---
|
|
18
|
+
|
|
12
19
|
<figure class:list={["gallery-plate", className]}>
|
|
13
20
|
<div class="plate-frame">
|
|
14
|
-
<LightboxLink
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
<LightboxLink
|
|
22
|
+
href={src.src}
|
|
23
|
+
w={src.width}
|
|
24
|
+
h={src.height}
|
|
25
|
+
caption={caption ?? alt}
|
|
26
|
+
>
|
|
27
|
+
<SmartImage
|
|
28
|
+
src={src}
|
|
29
|
+
alt={alt}
|
|
30
|
+
widths={[320, 640]}
|
|
31
|
+
sizes="(max-width: 600px) 100vw, 320px"
|
|
32
|
+
/>
|
|
33
|
+
<CornerBadges badges={badges} />
|
|
17
34
|
</LightboxLink>
|
|
18
35
|
</div>
|
|
19
36
|
{caption && <figcaption class="caption">{caption}</figcaption>}
|
|
@@ -22,9 +39,11 @@ const { src, alt = "", caption, genai = false, class: className } = Astro.props;
|
|
|
22
39
|
<style>
|
|
23
40
|
.gallery-plate { margin: 0; }
|
|
24
41
|
.plate-frame { border: var(--stroke) solid var(--rule); line-height: 0; }
|
|
42
|
+
|
|
25
43
|
/* The frame owns the outline; zero the default img border from
|
|
26
44
|
prose.css so plates don't double-line. */
|
|
27
45
|
.plate-frame :global(img) { width: 100%; height: 220px; object-fit: cover; border: 0; }
|
|
46
|
+
|
|
28
47
|
/* Type from the .caption pattern, tracked a touch wider under a plate.
|
|
29
48
|
Caption case is the author's own; the uppercase transform from the
|
|
30
49
|
original plate design was dropped by owner request 2026-07-26. */
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
pattern; only the outer margin is this component's. */
|
|
7
|
-
interface Props { class?: string }
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
8
6
|
const { class: className } = Astro.props;
|
|
9
7
|
---
|
|
8
|
+
|
|
10
9
|
<div class:list={["content-group", "rule-box", className]}><slot /></div>
|
|
11
10
|
|
|
12
11
|
<style>
|
|
@@ -1,24 +1,53 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ImageMetadata } from "astro";
|
|
3
3
|
import CornerBadges from "../CornerBadges.astro";
|
|
4
|
+
import type { Badge } from "../../scripts/core/badges";
|
|
4
5
|
import SmartImage from "../SmartImage.astro";
|
|
5
6
|
import LightboxLink from "../LightboxLink.astro";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
interface Props {
|
|
9
|
+
src: ImageMetadata;
|
|
10
|
+
alt?: string;
|
|
11
|
+
mediaSide?: "left" | "right";
|
|
12
|
+
natural?: boolean;
|
|
13
|
+
badges?: Badge[];
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
src,
|
|
19
|
+
alt = "",
|
|
20
|
+
mediaSide = "left",
|
|
21
|
+
natural = false,
|
|
22
|
+
badges = [],
|
|
23
|
+
class: className,
|
|
24
|
+
} = Astro.props;
|
|
11
25
|
---
|
|
12
|
-
|
|
26
|
+
|
|
27
|
+
<figure
|
|
28
|
+
class:list={[
|
|
29
|
+
"media-text",
|
|
30
|
+
mediaSide === "right" && "media-right",
|
|
31
|
+
natural && "media-natural",
|
|
32
|
+
className,
|
|
33
|
+
]}
|
|
34
|
+
>
|
|
13
35
|
<div class="media-text-media">
|
|
14
36
|
<LightboxLink href={src.src} w={src.width} h={src.height} caption={alt}>
|
|
15
|
-
{natural
|
|
37
|
+
{natural ? (
|
|
16
38
|
/* No responsive widths/sizes here: a `sizes` attribute makes the
|
|
17
39
|
browser report that size as the intrinsic width, which defeats
|
|
18
40
|
the natural-size rendering below. */
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
<SmartImage src={src} alt={alt} />
|
|
42
|
+
) : (
|
|
43
|
+
<SmartImage
|
|
44
|
+
src={src}
|
|
45
|
+
alt={alt}
|
|
46
|
+
widths={[480, 960]}
|
|
47
|
+
sizes="(max-width: 600px) 100vw, 480px"
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
<CornerBadges badges={badges} />
|
|
22
51
|
</LightboxLink>
|
|
23
52
|
</div>
|
|
24
53
|
<figcaption class="media-text-content caption"><slot /></figcaption>
|
|
@@ -34,22 +63,27 @@ const { src, alt = "", mediaSide = "left", natural = false, genai = false, class
|
|
|
34
63
|
}
|
|
35
64
|
.media-right .media-text-media { order: 2; }
|
|
36
65
|
.media-text-media { margin: 0; line-height: 0; }
|
|
66
|
+
|
|
37
67
|
/* The side text is the caption: type from the .caption pattern (owner
|
|
38
68
|
request 2026-08-14), left-aligned while it sits in its own column;
|
|
39
69
|
it centers only when stacked below. */
|
|
40
70
|
.media-text-content { text-align: left; }
|
|
41
71
|
.media-text-content :global(p) { margin: 0 0 0.5em; }
|
|
42
72
|
.media-text-content :global(p:last-child) { margin-bottom: 0; }
|
|
73
|
+
|
|
43
74
|
/* Outline comes from the .prose img standard in prose.css. */
|
|
44
75
|
.media-text-media :global(img) { width: 100%; height: auto; }
|
|
76
|
+
|
|
45
77
|
/* natural: opt-in for small diagrams and cartoons that look blown out
|
|
46
78
|
when stretched to the media column; renders at intrinsic size,
|
|
47
79
|
centered in the cell (owner request 2026-07-30). */
|
|
48
80
|
.media-natural .media-text-media { text-align: center; }
|
|
81
|
+
|
|
49
82
|
/* The link hugs the natural-size image so a corner badge lands on the
|
|
50
83
|
image, not the cell (same idiom as BlogImage). */
|
|
51
84
|
.media-natural .media-text-media :global(.lightbox-link) { width: fit-content; max-width: 100%; margin-inline: auto; }
|
|
52
85
|
.media-natural .media-text-media :global(img) { width: auto; max-width: 100%; }
|
|
86
|
+
|
|
53
87
|
@media (max-width: 600px) {
|
|
54
88
|
/* Stacked, the caption sits below the image like BlogImage's:
|
|
55
89
|
centered, hugging at the same 5px. */
|