@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
|
@@ -48,30 +48,46 @@ interface Popout {
|
|
|
48
48
|
closeOnFocusLeave?: boolean;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function wirePopout(
|
|
51
|
+
function wirePopout(
|
|
52
|
+
doc: Document,
|
|
53
|
+
{ btn, panel, openClass, onOpen, closeOnFocusLeave = false }: Popout,
|
|
54
|
+
): () => void {
|
|
52
55
|
const isOpen = (): boolean => panel.classList.contains(openClass);
|
|
53
|
-
|
|
56
|
+
|
|
57
|
+
const inside = (t: EventTarget | null): boolean =>
|
|
58
|
+
t instanceof Node && (panel.contains(t) || btn.contains(t));
|
|
59
|
+
|
|
54
60
|
const setOpen = (open: boolean): void => {
|
|
55
61
|
panel.classList.toggle(openClass, open);
|
|
56
62
|
btn.setAttribute("aria-expanded", String(open));
|
|
57
63
|
if (open) onOpen?.();
|
|
58
64
|
};
|
|
59
|
-
|
|
65
|
+
|
|
66
|
+
const onClick = (): void => {
|
|
67
|
+
setOpen(!isOpen());
|
|
68
|
+
};
|
|
69
|
+
|
|
60
70
|
const onPointerdown = (ev: PointerEvent): void => {
|
|
61
71
|
if (isOpen() && !inside(ev.target)) setOpen(false);
|
|
62
72
|
};
|
|
73
|
+
|
|
63
74
|
const onKeydown = (ev: KeyboardEvent): void => {
|
|
64
75
|
if (ev.key !== "Escape" || !isOpen()) return;
|
|
65
76
|
setOpen(false);
|
|
66
77
|
btn.focus();
|
|
67
78
|
};
|
|
79
|
+
|
|
68
80
|
const onFocusout = (ev: FocusEvent): void => {
|
|
69
|
-
if (isOpen() && ev.relatedTarget !== null && !inside(ev.relatedTarget))
|
|
81
|
+
if (isOpen() && ev.relatedTarget !== null && !inside(ev.relatedTarget)) {
|
|
82
|
+
setOpen(false);
|
|
83
|
+
}
|
|
70
84
|
};
|
|
85
|
+
|
|
71
86
|
btn.addEventListener("click", onClick);
|
|
72
87
|
doc.addEventListener("pointerdown", onPointerdown);
|
|
73
88
|
doc.addEventListener("keydown", onKeydown);
|
|
74
89
|
if (closeOnFocusLeave) panel.addEventListener("focusout", onFocusout);
|
|
90
|
+
|
|
75
91
|
return () => {
|
|
76
92
|
btn.removeEventListener("click", onClick);
|
|
77
93
|
doc.removeEventListener("pointerdown", onPointerdown);
|
|
@@ -80,7 +96,10 @@ function wirePopout(doc: Document, { btn, panel, openClass, onOpen, closeOnFocus
|
|
|
80
96
|
};
|
|
81
97
|
}
|
|
82
98
|
|
|
83
|
-
export const mountSiteHeader: Island<SiteHeaderOptions> = (
|
|
99
|
+
export const mountSiteHeader: Island<SiteHeaderOptions> = (
|
|
100
|
+
root,
|
|
101
|
+
options = {},
|
|
102
|
+
): IslandHandle => {
|
|
84
103
|
const {
|
|
85
104
|
dateId = "header-date",
|
|
86
105
|
menuButton = ".menu-toggle",
|
|
@@ -90,6 +109,7 @@ export const mountSiteHeader: Island<SiteHeaderOptions> = (root, options = {}):
|
|
|
90
109
|
searchField = ".search-field",
|
|
91
110
|
formatDate = formatHeaderDate,
|
|
92
111
|
} = options;
|
|
112
|
+
|
|
93
113
|
const doc = docOf(root);
|
|
94
114
|
|
|
95
115
|
// Header date: live like the WordPress original (build-time text is the
|
|
@@ -101,16 +121,23 @@ export const mountSiteHeader: Island<SiteHeaderOptions> = (root, options = {}):
|
|
|
101
121
|
|
|
102
122
|
const btn = root.querySelector<HTMLButtonElement>(menuButton);
|
|
103
123
|
const menu = doc.getElementById(menuId);
|
|
124
|
+
|
|
104
125
|
if (btn && menu && claim(btn, "site-header")) {
|
|
105
126
|
const off = wirePopout(doc, { btn, panel: menu, openClass: "open" });
|
|
106
|
-
|
|
127
|
+
|
|
128
|
+
unwire.push(() => {
|
|
129
|
+
off();
|
|
130
|
+
release(btn, "site-header");
|
|
131
|
+
});
|
|
107
132
|
}
|
|
108
133
|
|
|
109
134
|
const searchBtn = root.querySelector<HTMLButtonElement>(searchButton);
|
|
110
135
|
const wrap = searchBtn?.closest<HTMLElement>(searchWrap) ?? null;
|
|
136
|
+
|
|
111
137
|
if (searchBtn && wrap && claim(searchBtn, "site-header")) {
|
|
112
138
|
const field = wrap.querySelector<HTMLInputElement>(searchField);
|
|
113
139
|
wrap.setAttribute("data-search-js", "");
|
|
140
|
+
|
|
114
141
|
const off = wirePopout(doc, {
|
|
115
142
|
btn: searchBtn,
|
|
116
143
|
panel: wrap,
|
|
@@ -118,6 +145,7 @@ export const mountSiteHeader: Island<SiteHeaderOptions> = (root, options = {}):
|
|
|
118
145
|
onOpen: () => field?.focus(),
|
|
119
146
|
closeOnFocusLeave: true,
|
|
120
147
|
});
|
|
148
|
+
|
|
121
149
|
unwire.push(() => {
|
|
122
150
|
off();
|
|
123
151
|
wrap.removeAttribute("data-search-js");
|
package/src/scripts/subscribe.ts
CHANGED
|
@@ -35,13 +35,17 @@ export const MSG = {
|
|
|
35
35
|
A function member so an overridden register controls the whole
|
|
36
36
|
sentence, joiner included (step 9.5; the generalization review found
|
|
37
37
|
the joiner was the one baked phrase left in this script). */
|
|
38
|
-
failedAt: (failed: string, url: string): string =>
|
|
38
|
+
failedAt: (failed: string, url: string): string =>
|
|
39
|
+
`${failed.replace(/\.$/, "")}, or sign up at ${url}.`,
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
/* The provider's public sign-up page rides the form's data-public-url
|
|
42
43
|
(config NEWSLETTER via the component's publicUrl prop), so this script names no provider; without the
|
|
43
44
|
attribute the message stays generic. */
|
|
44
|
-
export function failedMessage(
|
|
45
|
+
export function failedMessage(
|
|
46
|
+
form: HTMLFormElement,
|
|
47
|
+
messages: typeof MSG,
|
|
48
|
+
): string {
|
|
45
49
|
const url = form.dataset.publicUrl;
|
|
46
50
|
return url ? messages.failedAt(messages.failed, url) : messages.failed;
|
|
47
51
|
}
|
|
@@ -51,11 +55,23 @@ function setStatus(status: HTMLElement, text: string, isError: boolean): void {
|
|
|
51
55
|
status.classList.toggle("subscribe-status-err", isError);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
async function send(
|
|
58
|
+
async function send(
|
|
59
|
+
form: HTMLFormElement,
|
|
60
|
+
input: HTMLInputElement,
|
|
61
|
+
button: HTMLButtonElement | null,
|
|
62
|
+
status: HTMLElement,
|
|
63
|
+
messages: typeof MSG,
|
|
64
|
+
): Promise<void> {
|
|
55
65
|
if (button) button.disabled = true;
|
|
56
66
|
setStatus(status, messages.pending, false);
|
|
67
|
+
|
|
57
68
|
try {
|
|
58
|
-
const res = await fetch(form.action, {
|
|
69
|
+
const res = await fetch(form.action, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: new FormData(form),
|
|
72
|
+
redirect: "manual",
|
|
73
|
+
});
|
|
74
|
+
|
|
59
75
|
if (res.ok || res.type === "opaqueredirect") {
|
|
60
76
|
setStatus(status, messages.sent, false);
|
|
61
77
|
input.value = "";
|
|
@@ -80,27 +96,52 @@ export interface SubscribeOptions {
|
|
|
80
96
|
messages?: Partial<typeof MSG>;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
|
-
export const mountSubscribe: Island<SubscribeOptions> = (
|
|
84
|
-
|
|
99
|
+
export const mountSubscribe: Island<SubscribeOptions> = (
|
|
100
|
+
root,
|
|
101
|
+
options = {},
|
|
102
|
+
): IslandHandle => {
|
|
103
|
+
const { selector = ".subscribe-form", messages: messagesOverride = {} } =
|
|
104
|
+
options;
|
|
105
|
+
|
|
85
106
|
const messages: typeof MSG = { ...MSG, ...messagesOverride };
|
|
86
|
-
|
|
107
|
+
|
|
108
|
+
const forms = [...root.querySelectorAll<HTMLFormElement>(selector)].filter(
|
|
109
|
+
(form) => claim(form, "subscribe"),
|
|
110
|
+
);
|
|
87
111
|
|
|
88
112
|
const handlers: [HTMLFormElement, (e: Event) => void][] = [];
|
|
113
|
+
|
|
89
114
|
for (const form of forms) {
|
|
90
115
|
const onSubmit = (e: Event): void => {
|
|
91
116
|
e.preventDefault();
|
|
92
117
|
const input = form.querySelector(".subscribe-email");
|
|
93
118
|
const button = form.querySelector(".subscribe-submit");
|
|
94
119
|
const status = form.parentElement?.querySelector(".subscribe-status");
|
|
95
|
-
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
!(input instanceof HTMLInputElement) ||
|
|
123
|
+
!(status instanceof HTMLElement)
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
96
128
|
const value = input.value.trim();
|
|
97
129
|
input.value = value;
|
|
130
|
+
|
|
98
131
|
if (!EMAIL.test(value)) {
|
|
99
132
|
setStatus(status, messages.invalid, true);
|
|
100
133
|
return;
|
|
101
134
|
}
|
|
102
|
-
|
|
135
|
+
|
|
136
|
+
void send(
|
|
137
|
+
form,
|
|
138
|
+
input,
|
|
139
|
+
button instanceof HTMLButtonElement ? button : null,
|
|
140
|
+
status,
|
|
141
|
+
messages,
|
|
142
|
+
);
|
|
103
143
|
};
|
|
144
|
+
|
|
104
145
|
form.addEventListener("submit", onSubmit);
|
|
105
146
|
handlers.push([form, onSubmit]);
|
|
106
147
|
}
|
|
@@ -22,8 +22,12 @@ const LABEL: Record<Theme, string> = {
|
|
|
22
22
|
every access is guarded and a bad value reads as no choice. With no
|
|
23
23
|
storageKey there is nowhere to read from, so this reads as no choice
|
|
24
24
|
too rather than guessing at a key. */
|
|
25
|
-
export function readStored(
|
|
25
|
+
export function readStored(
|
|
26
|
+
storage: Storage | null,
|
|
27
|
+
storageKey?: string,
|
|
28
|
+
): Theme | null {
|
|
26
29
|
if (!storageKey) return null;
|
|
30
|
+
|
|
27
31
|
try {
|
|
28
32
|
const v = storage?.getItem(storageKey);
|
|
29
33
|
return v === "light" || v === "dark" ? v : null;
|
|
@@ -36,8 +40,13 @@ export function readStored(storage: Storage | null, storageKey?: string): Theme
|
|
|
36
40
|
the page, it just does not persist the choice. That is a safe
|
|
37
41
|
degradation for an adopter who forgot to pass one, not a broken
|
|
38
42
|
toggle. */
|
|
39
|
-
function writeStored(
|
|
43
|
+
function writeStored(
|
|
44
|
+
storage: Storage | null,
|
|
45
|
+
storageKey: string | undefined,
|
|
46
|
+
theme: Theme,
|
|
47
|
+
): void {
|
|
40
48
|
if (!storageKey) return;
|
|
49
|
+
|
|
41
50
|
try {
|
|
42
51
|
storage?.setItem(storageKey, theme);
|
|
43
52
|
} catch {
|
|
@@ -56,7 +65,11 @@ export function apply(doc: Document, theme: Theme): void {
|
|
|
56
65
|
else delete doc.documentElement.dataset.theme;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
function reflect(
|
|
68
|
+
function reflect(
|
|
69
|
+
btn: HTMLButtonElement,
|
|
70
|
+
theme: Theme,
|
|
71
|
+
labels: Record<Theme, string>,
|
|
72
|
+
): void {
|
|
60
73
|
btn.setAttribute("aria-pressed", theme === "dark" ? "true" : "false");
|
|
61
74
|
btn.setAttribute("aria-label", labels[theme]);
|
|
62
75
|
btn.title = labels[theme];
|
|
@@ -79,23 +92,37 @@ export interface ThemeToggleOptions {
|
|
|
79
92
|
labels?: Record<Theme, string>;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
|
-
export const mountThemeToggle: Island<ThemeToggleOptions> = (
|
|
95
|
+
export const mountThemeToggle: Island<ThemeToggleOptions> = (
|
|
96
|
+
root,
|
|
97
|
+
options = {},
|
|
98
|
+
): IslandHandle => {
|
|
83
99
|
const doc = options.doc ?? docOf(root);
|
|
84
|
-
|
|
100
|
+
|
|
101
|
+
const {
|
|
102
|
+
selector = ".theme-toggle",
|
|
103
|
+
storage = safeStorage(doc),
|
|
104
|
+
storageKey,
|
|
105
|
+
labels = LABEL,
|
|
106
|
+
} = options;
|
|
107
|
+
|
|
85
108
|
const handlers: [HTMLButtonElement, () => void][] = [];
|
|
109
|
+
|
|
86
110
|
for (const btn of root.querySelectorAll<HTMLButtonElement>(selector)) {
|
|
87
111
|
if (mounted.has(btn)) continue;
|
|
88
112
|
mounted.add(btn);
|
|
89
113
|
reflect(btn, current(doc), labels);
|
|
114
|
+
|
|
90
115
|
const onClick = () => {
|
|
91
116
|
const next: Theme = current(doc) === "dark" ? "light" : "dark";
|
|
92
117
|
apply(doc, next);
|
|
93
118
|
writeStored(storage, storageKey, next);
|
|
94
119
|
for (const b of mounted) reflect(b, next, labels);
|
|
95
120
|
};
|
|
121
|
+
|
|
96
122
|
btn.addEventListener("click", onClick);
|
|
97
123
|
handlers.push([btn, onClick]);
|
|
98
124
|
}
|
|
125
|
+
|
|
99
126
|
return {
|
|
100
127
|
destroy(): void {
|
|
101
128
|
for (const [btn, onClick] of handlers) {
|
package/src/shiki/code-theme.mjs
CHANGED
|
@@ -7,10 +7,30 @@ export default {
|
|
|
7
7
|
"editor.foreground": "#e8d9c3",
|
|
8
8
|
},
|
|
9
9
|
tokenColors: [
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{
|
|
10
|
+
{
|
|
11
|
+
scope: ["keyword", "storage", "keyword.control"],
|
|
12
|
+
settings: { foreground: "#ffaa3c" },
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
scope: ["entity.name.function", "support.function", "entity.name.tag"],
|
|
16
|
+
settings: { foreground: "#ffd18a" },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
scope: [
|
|
20
|
+
"string",
|
|
21
|
+
"constant.numeric",
|
|
22
|
+
"constant.language",
|
|
23
|
+
"constant.character",
|
|
24
|
+
],
|
|
25
|
+
settings: { foreground: "#e07c14" },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
scope: ["comment", "punctuation.definition.comment"],
|
|
29
|
+
settings: { foreground: "#8a7a63", fontStyle: "italic" },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
scope: ["variable", "support.variable"],
|
|
33
|
+
settings: { foreground: "#e8d9c3" },
|
|
34
|
+
},
|
|
15
35
|
],
|
|
16
36
|
};
|
package/src/shiki/code-vars.mjs
CHANGED
|
@@ -19,7 +19,11 @@ const HEX_TO_VAR = {
|
|
|
19
19
|
function swap(node) {
|
|
20
20
|
const style = node.properties?.style;
|
|
21
21
|
if (typeof style !== "string") return;
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
node.properties.style = style.replace(
|
|
24
|
+
/#[0-9a-fA-F]{6}/g,
|
|
25
|
+
(hex) => HEX_TO_VAR[hex.toLowerCase()] ?? hex,
|
|
26
|
+
);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export default {
|