@treeseed/core 0.10.21 → 0.11.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 +69 -125
- package/dist/content.js +1 -0
- package/dist/dev-watch.js +2 -1
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +51 -35
- package/dist/pages/404.astro +1 -1
- package/dist/pages/[slug].astro +4 -4
- package/dist/pages/agents/[slug].astro +3 -3
- package/dist/pages/agents/index.astro +3 -3
- package/dist/pages/books/[slug].astro +3 -3
- package/dist/pages/books/index.astro +3 -3
- package/dist/pages/contact.astro +2 -2
- package/dist/pages/decisions/[slug].astro +3 -3
- package/dist/pages/decisions/index.astro +3 -3
- package/dist/pages/docs-runtime/[...slug].astro +3 -3
- package/dist/pages/docs-runtime/index.astro +3 -3
- package/dist/pages/index.astro +11 -11
- package/dist/pages/notes/[slug].astro +3 -3
- package/dist/pages/notes/index.astro +3 -3
- package/dist/pages/objectives/[slug].astro +3 -3
- package/dist/pages/objectives/index.astro +3 -3
- package/dist/pages/people/[slug].astro +3 -3
- package/dist/pages/people/index.astro +3 -3
- package/dist/pages/proposals/[slug].astro +3 -3
- package/dist/pages/proposals/index.astro +3 -3
- package/dist/pages/questions/[slug].astro +3 -3
- package/dist/pages/questions/index.astro +3 -3
- package/dist/pages/ui/index.astro +23 -23
- package/dist/platform-resources.js +5 -1
- package/dist/scripts/build-dist.js +2 -0
- package/dist/scripts/release-verify.js +24 -2
- package/dist/scripts/workspace-bootstrap.js +3 -0
- package/dist/site.js +49 -11
- package/dist/styles/global.css +5 -5
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +8 -6
- package/package.json +3 -45
- package/templates/github/deploy-web.workflow.yml +36 -2
- package/dist/components/DevWatchReload.astro +0 -45
- package/dist/components/SiteTitle.astro +0 -51
- package/dist/components/content/ContentStatusLegend.astro +0 -18
- package/dist/components/content/StatusBadge.astro +0 -11
- package/dist/components/docs/BookFontControls.astro +0 -180
- package/dist/components/docs/DesktopSidebarToggle.astro +0 -88
- package/dist/components/docs/DownloadBook.astro +0 -34
- package/dist/components/docs/Footer.astro +0 -112
- package/dist/components/docs/Header.astro +0 -157
- package/dist/components/docs/PageFrame.astro +0 -260
- package/dist/components/docs/PageSidebar.astro +0 -63
- package/dist/components/docs/PageTitle.astro +0 -39
- package/dist/components/docs/Sidebar.astro +0 -41
- package/dist/components/docs/ThemeSelect.astro +0 -5
- package/dist/components/forms/ContactForm.astro +0 -233
- package/dist/components/forms/FooterSubscribeForm.astro +0 -188
- package/dist/components/site/BookList.astro +0 -27
- package/dist/components/site/CTASection.astro +0 -24
- package/dist/components/site/ChronicleList.astro +0 -33
- package/dist/components/site/Hero.astro +0 -18
- package/dist/components/site/NotesList.astro +0 -29
- package/dist/components/site/PathCard.astro +0 -16
- package/dist/components/site/ProfileList.astro +0 -30
- package/dist/components/site/PublishedContentBody.astro +0 -5
- package/dist/components/site/RouteNotFound.astro +0 -25
- package/dist/components/site/SectionIntro.astro +0 -9
- package/dist/components/site/StageBanner.astro +0 -8
- package/dist/components/site/TrustCallout.astro +0 -9
- package/dist/components/starlight.js +0 -6
- package/dist/components/ui/data/ActionList.astro +0 -51
- package/dist/components/ui/data/Badge.astro +0 -19
- package/dist/components/ui/data/DataTable.astro +0 -51
- package/dist/components/ui/data/KeyValueList.astro +0 -28
- package/dist/components/ui/data/MetricCard.astro +0 -25
- package/dist/components/ui/data/MetricGrid.astro +0 -27
- package/dist/components/ui/data/StatusPill.astro +0 -20
- package/dist/components/ui/forms/Button.astro +0 -59
- package/dist/components/ui/forms/Field.astro +0 -39
- package/dist/components/ui/forms/FormActions.astro +0 -12
- package/dist/components/ui/forms/PasswordMeter.astro +0 -80
- package/dist/components/ui/forms/RadioGroup.astro +0 -55
- package/dist/components/ui/forms/Select.astro +0 -47
- package/dist/components/ui/forms/TextInput.astro +0 -58
- package/dist/components/ui/forms/Textarea.astro +0 -45
- package/dist/components/ui/layout/PageHeader.astro +0 -45
- package/dist/components/ui/shell/AppShell.astro +0 -130
- package/dist/components/ui/shell/BottomNav.astro +0 -42
- package/dist/components/ui/shell/ProjectHeader.astro +0 -66
- package/dist/components/ui/shell/PublicFooter.astro +0 -39
- package/dist/components/ui/shell/PublicShell.astro +0 -184
- package/dist/components/ui/shell/RailNav.astro +0 -42
- package/dist/components/ui/shell/ShellIconLink.astro +0 -30
- package/dist/components/ui/shell/TopBar.astro +0 -52
- package/dist/components/ui/surface/Card.astro +0 -46
- package/dist/components/ui/surface/EmptyState.astro +0 -45
- package/dist/components/ui/surface/Panel.astro +0 -54
- package/dist/components/ui/theme/ThemeMenu.astro +0 -58
- package/dist/components/ui/theme/ThemePreviewSwatch.astro +0 -18
- package/dist/components/ui/theme/ThemeScript.astro +0 -112
- package/dist/components/ui/theme/ThemeSelector.astro +0 -202
- package/dist/components/ui/types.js +0 -0
- package/dist/layouts/AuthoredEntryLayout.astro +0 -195
- package/dist/layouts/BookLayout.astro +0 -35
- package/dist/layouts/BridgeLayout.astro +0 -11
- package/dist/layouts/ContentLayout.astro +0 -24
- package/dist/layouts/MainLayout.astro +0 -76
- package/dist/layouts/NoteLayout.astro +0 -26
- package/dist/layouts/ProfileLayout.astro +0 -85
- package/dist/styles/app-shell.css +0 -626
- package/dist/styles/forms.css +0 -274
- package/dist/styles/theme.css +0 -198
- package/dist/styles/tokens.css +0 -65
- package/dist/styles/ui.css +0 -551
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import {
|
|
3
|
-
TREESEED_PUBLIC_TURNSTILE_SITE_KEY,
|
|
4
|
-
} from 'astro:env/client';
|
|
5
|
-
import { CONTACT_TYPE_LABELS } from '../../utils/forms/constants';
|
|
6
|
-
import { CONTACT_TYPES } from '../../types/forms';
|
|
7
|
-
import { SITE_FORMS } from '../../utils/site-config';
|
|
8
|
-
|
|
9
|
-
const { status, code } = Astro.props as {
|
|
10
|
-
status?: string | null;
|
|
11
|
-
code?: string | null;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const messages: Record<string, string> = {
|
|
15
|
-
success: 'Your message was sent successfully.',
|
|
16
|
-
invalid_request: 'The request could not be accepted. Please refresh and try again.',
|
|
17
|
-
invalid_form: 'Please complete the required fields and try again.',
|
|
18
|
-
captcha_failed: 'Please complete the verification challenge and try again.',
|
|
19
|
-
token_invalid: 'The form session expired. Refresh the page and try again.',
|
|
20
|
-
token_expired: 'The form session expired. Refresh the page and try again.',
|
|
21
|
-
token_replayed: 'This form token has already been used. Please refresh the page.',
|
|
22
|
-
rate_limited: 'Too many attempts were detected. Please wait a few minutes and try again.',
|
|
23
|
-
delivery_failed: 'We could not deliver the message right now. Please try again shortly.',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const statusMessage = code ? messages[code] ?? null : null;
|
|
27
|
-
const shouldBypassTurnstile = import.meta.env.DEV;
|
|
28
|
-
const formsApiBaseUrl = SITE_FORMS?.apiBaseUrl ?? '/api/form/submit';
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
<section class="max-w-3xl">
|
|
32
|
-
{statusMessage && (
|
|
33
|
-
<div class:list={[
|
|
34
|
-
'rounded-lg border px-4 py-3 text-sm font-medium',
|
|
35
|
-
status === 'success'
|
|
36
|
-
? 'border-[color:var(--ts-color-info)] bg-[color:var(--ts-color-info-soft)] text-[color:var(--ts-color-text)]'
|
|
37
|
-
: 'border-[color:var(--ts-color-warning)] bg-[color:var(--ts-color-warning-soft)] text-[color:var(--ts-color-text)]',
|
|
38
|
-
]}>
|
|
39
|
-
{statusMessage}
|
|
40
|
-
</div>
|
|
41
|
-
)}
|
|
42
|
-
|
|
43
|
-
<form
|
|
44
|
-
method="post"
|
|
45
|
-
action={formsApiBaseUrl}
|
|
46
|
-
class="js-secure-form mt-6 grid gap-5"
|
|
47
|
-
data-form-type="contact"
|
|
48
|
-
data-turnstile-action="contact_submit"
|
|
49
|
-
>
|
|
50
|
-
<p class="text-sm text-[color:var(--ts-color-text-subtle)]">
|
|
51
|
-
Fields marked <span class="font-semibold text-[color:var(--ts-color-warning)]">*</span> are required.
|
|
52
|
-
</p>
|
|
53
|
-
<input type="hidden" name="formType" value="contact" />
|
|
54
|
-
<input type="hidden" name="formToken" value="" />
|
|
55
|
-
<input type="hidden" name="formSession" value="" />
|
|
56
|
-
<input type="hidden" name="redirectTo" value="/contact/" />
|
|
57
|
-
<div class="sr-only" aria-hidden="true">
|
|
58
|
-
<label>
|
|
59
|
-
Website
|
|
60
|
-
<input type="text" name="website" tabindex="-1" autocomplete="off" />
|
|
61
|
-
</label>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<div class="grid gap-5 md:grid-cols-2">
|
|
65
|
-
<label class="grid gap-2">
|
|
66
|
-
<span class="text-sm font-semibold text-[color:var(--ts-color-text)]">Name <span aria-hidden="true" class="text-[color:var(--ts-color-warning)]">*</span></span>
|
|
67
|
-
<input
|
|
68
|
-
class="min-h-12 rounded-md border border-[color:var(--ts-color-border-strong)] bg-white/70 px-4 py-3"
|
|
69
|
-
type="text"
|
|
70
|
-
name="name"
|
|
71
|
-
autocomplete="name"
|
|
72
|
-
required
|
|
73
|
-
aria-required="true"
|
|
74
|
-
/>
|
|
75
|
-
</label>
|
|
76
|
-
<label class="grid gap-2">
|
|
77
|
-
<span class="text-sm font-semibold text-[color:var(--ts-color-text)]">Email <span aria-hidden="true" class="text-[color:var(--ts-color-warning)]">*</span></span>
|
|
78
|
-
<input
|
|
79
|
-
class="min-h-12 rounded-md border border-[color:var(--ts-color-border-strong)] bg-white/70 px-4 py-3"
|
|
80
|
-
type="email"
|
|
81
|
-
name="email"
|
|
82
|
-
autocomplete="email"
|
|
83
|
-
required
|
|
84
|
-
aria-required="true"
|
|
85
|
-
/>
|
|
86
|
-
</label>
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
<div class="grid gap-5 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
|
90
|
-
<label class="grid gap-2">
|
|
91
|
-
<span class="text-sm font-semibold text-[color:var(--ts-color-text)]">Organization</span>
|
|
92
|
-
<input
|
|
93
|
-
class="min-h-12 rounded-md border border-[color:var(--ts-color-border-strong)] bg-white/70 px-4 py-3"
|
|
94
|
-
type="text"
|
|
95
|
-
name="organization"
|
|
96
|
-
autocomplete="organization"
|
|
97
|
-
/>
|
|
98
|
-
</label>
|
|
99
|
-
<label class="grid gap-2">
|
|
100
|
-
<span class="text-sm font-semibold text-[color:var(--ts-color-text)]">Topic <span aria-hidden="true" class="text-[color:var(--ts-color-warning)]">*</span></span>
|
|
101
|
-
<select
|
|
102
|
-
class="ts-control ts-control--select min-h-12 rounded-md border border-[color:var(--ts-color-border-strong)] px-4 py-3"
|
|
103
|
-
name="contactType"
|
|
104
|
-
required
|
|
105
|
-
aria-required="true"
|
|
106
|
-
>
|
|
107
|
-
{CONTACT_TYPES.map((contactType) => (
|
|
108
|
-
<option value={contactType}>{CONTACT_TYPE_LABELS[contactType]}</option>
|
|
109
|
-
))}
|
|
110
|
-
</select>
|
|
111
|
-
</label>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<label class="grid gap-2">
|
|
115
|
-
<span class="text-sm font-semibold text-[color:var(--ts-color-text)]">Subject <span aria-hidden="true" class="text-[color:var(--ts-color-warning)]">*</span></span>
|
|
116
|
-
<input
|
|
117
|
-
class="min-h-12 rounded-md border border-[color:var(--ts-color-border-strong)] bg-white/70 px-4 py-3"
|
|
118
|
-
type="text"
|
|
119
|
-
name="subject"
|
|
120
|
-
required
|
|
121
|
-
aria-required="true"
|
|
122
|
-
/>
|
|
123
|
-
</label>
|
|
124
|
-
|
|
125
|
-
<label class="grid gap-2">
|
|
126
|
-
<span class="text-sm font-semibold text-[color:var(--ts-color-text)]">Message <span aria-hidden="true" class="text-[color:var(--ts-color-warning)]">*</span></span>
|
|
127
|
-
<textarea
|
|
128
|
-
class="min-h-48 rounded-md border border-[color:var(--ts-color-border-strong)] bg-white/70 px-4 py-3"
|
|
129
|
-
name="message"
|
|
130
|
-
required
|
|
131
|
-
aria-required="true"
|
|
132
|
-
></textarea>
|
|
133
|
-
</label>
|
|
134
|
-
|
|
135
|
-
{TREESEED_PUBLIC_TURNSTILE_SITE_KEY && !shouldBypassTurnstile ? (
|
|
136
|
-
<div class="js-turnstile rounded-md border border-dashed border-[color:var(--ts-color-border-strong)] p-3"></div>
|
|
137
|
-
) : shouldBypassTurnstile ? (
|
|
138
|
-
<p class="rounded-md border border-[color:var(--ts-color-info)] bg-[color:var(--ts-color-info-soft)] px-4 py-3 text-sm text-[color:var(--ts-color-text)]">
|
|
139
|
-
Local development mode is bypassing Turnstile so you can test the form workflow without Cloudflare captcha.
|
|
140
|
-
</p>
|
|
141
|
-
) : (
|
|
142
|
-
<p class="rounded-md border border-[color:var(--ts-color-warning)] bg-[color:var(--ts-color-warning-soft)] px-4 py-3 text-sm text-[color:var(--ts-color-text)]">
|
|
143
|
-
Turnstile is not configured yet. Add `TREESEED_PUBLIC_TURNSTILE_SITE_KEY` before deploying forms.
|
|
144
|
-
</p>
|
|
145
|
-
)}
|
|
146
|
-
|
|
147
|
-
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
148
|
-
<p class="text-sm leading-7 text-[color:var(--ts-color-text-subtle)]">
|
|
149
|
-
By sending this message, you agree that we may reply by email about this inquiry.
|
|
150
|
-
</p>
|
|
151
|
-
<button
|
|
152
|
-
type="submit"
|
|
153
|
-
class="js-submit inline-flex min-h-12 items-center justify-center border border-[color:var(--ts-color-accent)] bg-[color:var(--ts-color-accent)] px-6 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)] disabled:cursor-not-allowed disabled:opacity-60"
|
|
154
|
-
disabled
|
|
155
|
-
>
|
|
156
|
-
Send message
|
|
157
|
-
</button>
|
|
158
|
-
</div>
|
|
159
|
-
</form>
|
|
160
|
-
</section>
|
|
161
|
-
|
|
162
|
-
<script
|
|
163
|
-
is:inline
|
|
164
|
-
define:vars={{ siteKey: TREESEED_PUBLIC_TURNSTILE_SITE_KEY, shouldBypassTurnstile, formsApiBaseUrl }}
|
|
165
|
-
>
|
|
166
|
-
const bootSecureForms = () => {
|
|
167
|
-
const forms = Array.from(document.querySelectorAll('.js-secure-form'));
|
|
168
|
-
|
|
169
|
-
for (const form of forms) {
|
|
170
|
-
if (!(form instanceof HTMLFormElement) || form.dataset.ready === 'true') continue;
|
|
171
|
-
form.dataset.ready = 'true';
|
|
172
|
-
|
|
173
|
-
const formType = form.dataset.formType;
|
|
174
|
-
const action = form.dataset.turnstileAction ?? '';
|
|
175
|
-
const tokenField = form.querySelector('input[name="formToken"]');
|
|
176
|
-
const sessionField = form.querySelector('input[name="formSession"]');
|
|
177
|
-
const submitButton = form.querySelector('.js-submit');
|
|
178
|
-
const widgetContainer = form.querySelector('.js-turnstile');
|
|
179
|
-
|
|
180
|
-
const enableSubmit = () => {
|
|
181
|
-
if (submitButton instanceof HTMLButtonElement) {
|
|
182
|
-
submitButton.disabled = !(tokenField instanceof HTMLInputElement && tokenField.value);
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const assignToken = async () => {
|
|
187
|
-
const response = await fetch(`${formsApiBaseUrl}?formType=${encodeURIComponent(formType ?? '')}`, {
|
|
188
|
-
headers: { accept: 'application/json' },
|
|
189
|
-
credentials: 'same-origin',
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const data = await response.json();
|
|
193
|
-
if (tokenField instanceof HTMLInputElement && data?.formToken) {
|
|
194
|
-
tokenField.value = data.formToken;
|
|
195
|
-
}
|
|
196
|
-
if (sessionField instanceof HTMLInputElement && data?.sessionId) {
|
|
197
|
-
sessionField.value = data.sessionId;
|
|
198
|
-
}
|
|
199
|
-
enableSubmit();
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const renderWidget = () => {
|
|
203
|
-
if (shouldBypassTurnstile) return;
|
|
204
|
-
if (!siteKey || !(widgetContainer instanceof HTMLElement)) return;
|
|
205
|
-
if (!('turnstile' in window) || typeof window.turnstile?.render !== 'function') return;
|
|
206
|
-
if (widgetContainer.dataset.rendered === 'true') return;
|
|
207
|
-
|
|
208
|
-
window.turnstile.render(widgetContainer, {
|
|
209
|
-
sitekey: siteKey,
|
|
210
|
-
action,
|
|
211
|
-
theme: 'light',
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
widgetContainer.dataset.rendered = 'true';
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
assignToken().catch((error) => {
|
|
218
|
-
console.error('Unable to initialize form token', error);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
if (siteKey) {
|
|
222
|
-
renderWidget();
|
|
223
|
-
window.addEventListener('load', renderWidget, { once: true });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
if (document.readyState === 'loading') {
|
|
229
|
-
document.addEventListener('DOMContentLoaded', bootSecureForms, { once: true });
|
|
230
|
-
} else {
|
|
231
|
-
bootSecureForms();
|
|
232
|
-
}
|
|
233
|
-
</script>
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import {
|
|
3
|
-
TREESEED_PUBLIC_TURNSTILE_SITE_KEY,
|
|
4
|
-
} from 'astro:env/client';
|
|
5
|
-
import { FORM_CODE_PARAM, FORM_SUCCESS_PARAM, SUBSCRIBE_ANCHOR_ID } from '../../utils/forms/constants';
|
|
6
|
-
import { SITE_FORMS } from '../../utils/site-config';
|
|
7
|
-
|
|
8
|
-
const { currentPath = '/' } = Astro.props as { currentPath?: string };
|
|
9
|
-
const redirectTarget = `${currentPath}#${SUBSCRIBE_ANCHOR_ID}`;
|
|
10
|
-
const shouldBypassTurnstile = import.meta.env.DEV;
|
|
11
|
-
const formsApiBaseUrl = SITE_FORMS?.apiBaseUrl ?? '/api/form/submit';
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
<section id={SUBSCRIBE_ANCHOR_ID} class="mt-6 border-t border-[color:var(--ts-color-border)] pt-4">
|
|
15
|
-
<div class="flex flex-col items-center gap-3 text-center">
|
|
16
|
-
<div class="flex flex-col gap-1">
|
|
17
|
-
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[color:var(--ts-color-info-text)]">Subscribe for updates</p>
|
|
18
|
-
<p class="text-sm text-[color:var(--ts-color-text-muted)]">
|
|
19
|
-
Get occasional notes when new writing or project updates are published.
|
|
20
|
-
</p>
|
|
21
|
-
</div>
|
|
22
|
-
<p class="js-subscribe-status hidden rounded-md border px-4 py-2 text-sm font-medium"></p>
|
|
23
|
-
<form
|
|
24
|
-
method="post"
|
|
25
|
-
action={formsApiBaseUrl}
|
|
26
|
-
class="js-secure-form flex w-full max-w-4xl flex-col items-center gap-3"
|
|
27
|
-
data-form-type="subscribe"
|
|
28
|
-
data-turnstile-action="subscribe_submit"
|
|
29
|
-
>
|
|
30
|
-
<input type="hidden" name="formType" value="subscribe" />
|
|
31
|
-
<input type="hidden" name="formToken" value="" />
|
|
32
|
-
<input type="hidden" name="formSession" value="" />
|
|
33
|
-
<input type="hidden" name="redirectTo" value={redirectTarget} />
|
|
34
|
-
<div class="sr-only" aria-hidden="true">
|
|
35
|
-
<label>
|
|
36
|
-
Website
|
|
37
|
-
<input type="text" name="website" tabindex="-1" autocomplete="off" />
|
|
38
|
-
</label>
|
|
39
|
-
</div>
|
|
40
|
-
<input type="hidden" name="name" value="" />
|
|
41
|
-
<div class="flex w-full flex-col items-center justify-center gap-3 md:flex-row md:flex-wrap">
|
|
42
|
-
<input
|
|
43
|
-
class="min-h-11 w-full rounded-md border border-[color:var(--ts-color-border-strong)] bg-white/80 px-4 py-2 md:w-auto md:min-w-[16rem] md:max-w-[18rem]"
|
|
44
|
-
type="email"
|
|
45
|
-
name="email"
|
|
46
|
-
placeholder="Email address"
|
|
47
|
-
autocomplete="email"
|
|
48
|
-
required
|
|
49
|
-
/>
|
|
50
|
-
{TREESEED_PUBLIC_TURNSTILE_SITE_KEY && !shouldBypassTurnstile ? (
|
|
51
|
-
<div class="js-turnstile min-h-0"></div>
|
|
52
|
-
) : shouldBypassTurnstile ? (
|
|
53
|
-
<p class="text-xs text-[color:var(--ts-color-text-subtle)]">Captcha bypassed locally.</p>
|
|
54
|
-
) : (
|
|
55
|
-
<p class="text-xs text-[color:var(--ts-color-text-subtle)]">Turnstile not configured.</p>
|
|
56
|
-
)}
|
|
57
|
-
<button
|
|
58
|
-
type="submit"
|
|
59
|
-
class="js-submit inline-flex min-h-11 items-center justify-center whitespace-nowrap border border-[color:var(--ts-color-accent)] bg-[color:var(--ts-color-accent)] px-4 py-2 text-sm font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)] disabled:cursor-not-allowed disabled:opacity-60"
|
|
60
|
-
disabled
|
|
61
|
-
>
|
|
62
|
-
Subscribe
|
|
63
|
-
</button>
|
|
64
|
-
</div>
|
|
65
|
-
</form>
|
|
66
|
-
</div>
|
|
67
|
-
</section>
|
|
68
|
-
|
|
69
|
-
{TREESEED_PUBLIC_TURNSTILE_SITE_KEY && !shouldBypassTurnstile && (
|
|
70
|
-
<script is:inline src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
|
|
71
|
-
)}
|
|
72
|
-
|
|
73
|
-
<script
|
|
74
|
-
is:inline
|
|
75
|
-
define:vars={{
|
|
76
|
-
siteKey: TREESEED_PUBLIC_TURNSTILE_SITE_KEY,
|
|
77
|
-
shouldBypassTurnstile,
|
|
78
|
-
formsApiBaseUrl,
|
|
79
|
-
successParam: FORM_SUCCESS_PARAM,
|
|
80
|
-
codeParam: FORM_CODE_PARAM,
|
|
81
|
-
subscribeAnchorId: SUBSCRIBE_ANCHOR_ID,
|
|
82
|
-
}}
|
|
83
|
-
>
|
|
84
|
-
const subscribeMessages = {
|
|
85
|
-
success: 'You are subscribed. Thanks for following along.',
|
|
86
|
-
invalid_form: 'Please provide your email address.',
|
|
87
|
-
captcha_failed: 'Please complete the verification challenge and try again.',
|
|
88
|
-
token_invalid: 'Please refresh the page and try again.',
|
|
89
|
-
token_expired: 'Please refresh the page and try again.',
|
|
90
|
-
rate_limited: 'Too many attempts were detected. Please wait a few minutes.',
|
|
91
|
-
delivery_failed: 'We could not finish the signup right now. Please try again shortly.',
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const syncSubscribeStatus = () => {
|
|
95
|
-
const statusNode = document.querySelector('.js-subscribe-status');
|
|
96
|
-
if (!(statusNode instanceof HTMLElement)) return;
|
|
97
|
-
|
|
98
|
-
const params = new URLSearchParams(window.location.search);
|
|
99
|
-
const status = params.get(successParam);
|
|
100
|
-
const code = params.get(codeParam);
|
|
101
|
-
|
|
102
|
-
if (!status || !code || window.location.hash !== `#${subscribeAnchorId}`) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
statusNode.textContent = subscribeMessages[code] ?? (status === 'success' ? 'Subscription complete.' : 'We could not complete the signup.');
|
|
107
|
-
statusNode.classList.remove('hidden');
|
|
108
|
-
if (status === 'success') {
|
|
109
|
-
statusNode.className = 'js-subscribe-status rounded-md border border-[color:var(--ts-color-info)] bg-[color:var(--ts-color-info-soft)] px-4 py-2 text-sm font-medium text-[color:var(--ts-color-text)]';
|
|
110
|
-
} else {
|
|
111
|
-
statusNode.className = 'js-subscribe-status rounded-md border border-[color:var(--ts-color-warning)] bg-[color:var(--ts-color-warning-soft)] px-4 py-2 text-sm font-medium text-[color:var(--ts-color-text)]';
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
if (document.readyState === 'loading') {
|
|
116
|
-
document.addEventListener('DOMContentLoaded', syncSubscribeStatus, { once: true });
|
|
117
|
-
} else {
|
|
118
|
-
syncSubscribeStatus();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const bootSecureForms = () => {
|
|
122
|
-
const forms = Array.from(document.querySelectorAll('.js-secure-form'));
|
|
123
|
-
|
|
124
|
-
for (const form of forms) {
|
|
125
|
-
if (!(form instanceof HTMLFormElement) || form.dataset.ready === 'true') continue;
|
|
126
|
-
form.dataset.ready = 'true';
|
|
127
|
-
|
|
128
|
-
const formType = form.dataset.formType;
|
|
129
|
-
const action = form.dataset.turnstileAction ?? '';
|
|
130
|
-
const tokenField = form.querySelector('input[name="formToken"]');
|
|
131
|
-
const sessionField = form.querySelector('input[name="formSession"]');
|
|
132
|
-
const submitButton = form.querySelector('.js-submit');
|
|
133
|
-
const widgetContainer = form.querySelector('.js-turnstile');
|
|
134
|
-
|
|
135
|
-
const enableSubmit = () => {
|
|
136
|
-
if (submitButton instanceof HTMLButtonElement) {
|
|
137
|
-
submitButton.disabled = !(tokenField instanceof HTMLInputElement && tokenField.value);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const assignToken = async () => {
|
|
142
|
-
const response = await fetch(`${formsApiBaseUrl}?formType=${encodeURIComponent(formType ?? '')}`, {
|
|
143
|
-
headers: { accept: 'application/json' },
|
|
144
|
-
credentials: 'same-origin',
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const data = await response.json();
|
|
148
|
-
if (tokenField instanceof HTMLInputElement && data?.formToken) {
|
|
149
|
-
tokenField.value = data.formToken;
|
|
150
|
-
}
|
|
151
|
-
if (sessionField instanceof HTMLInputElement && data?.sessionId) {
|
|
152
|
-
sessionField.value = data.sessionId;
|
|
153
|
-
}
|
|
154
|
-
enableSubmit();
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const renderWidget = () => {
|
|
158
|
-
if (shouldBypassTurnstile) return;
|
|
159
|
-
if (!siteKey || !(widgetContainer instanceof HTMLElement)) return;
|
|
160
|
-
if (!('turnstile' in window) || typeof window.turnstile?.render !== 'function') return;
|
|
161
|
-
if (widgetContainer.dataset.rendered === 'true') return;
|
|
162
|
-
|
|
163
|
-
window.turnstile.render(widgetContainer, {
|
|
164
|
-
sitekey: siteKey,
|
|
165
|
-
action,
|
|
166
|
-
theme: 'light',
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
widgetContainer.dataset.rendered = 'true';
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
assignToken().catch((error) => {
|
|
173
|
-
console.error('Unable to initialize form token', error);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
if (siteKey) {
|
|
177
|
-
renderWidget();
|
|
178
|
-
window.addEventListener('load', renderWidget, { once: true });
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
if (document.readyState === 'loading') {
|
|
184
|
-
document.addEventListener('DOMContentLoaded', bootSecureForms, { once: true });
|
|
185
|
-
} else {
|
|
186
|
-
bootSecureForms();
|
|
187
|
-
}
|
|
188
|
-
</script>
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type BookItem = {
|
|
3
|
-
href: string;
|
|
4
|
-
title: string;
|
|
5
|
-
summary: string;
|
|
6
|
-
meta: string;
|
|
7
|
-
landingPath: string;
|
|
8
|
-
downloadHref: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const { items } = Astro.props as { items: BookItem[] };
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
<div class="grid gap-6 md:grid-cols-2">
|
|
15
|
-
{items.map((item) => (
|
|
16
|
-
<div class="border border-[color:var(--ts-color-border)] bg-[color:var(--ts-color-surface)] p-6">
|
|
17
|
-
<p class="text-sm font-semibold uppercase tracking-[0.14em] text-[color:var(--ts-color-info-text)]">{item.meta}</p>
|
|
18
|
-
<h3 class="mt-4 text-2xl font-bold text-[color:var(--ts-color-text)]">{item.title}</h3>
|
|
19
|
-
<p class="mt-3 text-base leading-8 text-[color:var(--ts-color-text-muted)]">{item.summary}</p>
|
|
20
|
-
<div class="mt-5 flex flex-wrap gap-3">
|
|
21
|
-
<a href={item.href} class="border border-[color:var(--ts-color-border-strong)] px-4 py-2 text-sm font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">Book page</a>
|
|
22
|
-
<a href={item.landingPath} class="border border-[color:var(--ts-color-border-strong)] px-4 py-2 text-sm font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">Open knowledge</a>
|
|
23
|
-
<a href={item.downloadHref} class="border border-[color:var(--ts-color-accent)] bg-[color:var(--ts-color-accent)] px-4 py-2 text-sm font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">Download</a>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
))}
|
|
27
|
-
</div>
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
const { title, body, primaryHref, primaryLabel, secondaryHref, secondaryLabel } = Astro.props;
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
<section class="site-footer-cta px-0 py-0 text-[color:var(--ts-color-text)]">
|
|
6
|
-
<div class="site-footer-cta__inner">
|
|
7
|
-
<div class="mx-auto flex max-w-4xl flex-col items-center gap-6">
|
|
8
|
-
<div class="max-w-3xl">
|
|
9
|
-
<h2 class="font-serif text-3xl font-bold md:text-4xl">{title}</h2>
|
|
10
|
-
<p class="mt-4 text-lg leading-9 text-[color:var(--ts-color-text-muted)]">{body}</p>
|
|
11
|
-
</div>
|
|
12
|
-
<div class="flex flex-wrap justify-center gap-3">
|
|
13
|
-
<a href={primaryHref} class="border border-[color:var(--ts-color-accent)] bg-[color:var(--ts-color-accent)] px-5 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">{
|
|
14
|
-
primaryLabel
|
|
15
|
-
}</a>
|
|
16
|
-
{secondaryHref && secondaryLabel && (
|
|
17
|
-
<a href={secondaryHref} class="border border-[color:var(--ts-color-border-strong)] bg-[color:var(--ts-color-surface-muted)] px-5 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">
|
|
18
|
-
{secondaryLabel}
|
|
19
|
-
</a>
|
|
20
|
-
)}
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</section>
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { ContentStatus } from '../../utils/content-status';
|
|
3
|
-
import StatusBadge from '../content/StatusBadge.astro';
|
|
4
|
-
|
|
5
|
-
type ChronicleItem = {
|
|
6
|
-
href: string;
|
|
7
|
-
title: string;
|
|
8
|
-
summary: string;
|
|
9
|
-
status: ContentStatus;
|
|
10
|
-
date: Date;
|
|
11
|
-
meta?: string;
|
|
12
|
-
tags?: string[];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const { items } = Astro.props as { items: ChronicleItem[] };
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
<div class="grid gap-1">
|
|
19
|
-
{items.map((item) => (
|
|
20
|
-
<a href={item.href} class="block border-t border-[color:var(--ts-color-border)] py-7 transition hover:border-[color:var(--ts-color-border-strong)]">
|
|
21
|
-
<div class="flex flex-wrap items-center gap-4">
|
|
22
|
-
<StatusBadge status={item.status} />
|
|
23
|
-
<p class="text-sm font-medium text-[color:var(--ts-color-text-subtle)]">{item.date.toISOString().slice(0, 10)}</p>
|
|
24
|
-
{item.meta && <p class="text-sm text-[color:var(--ts-color-text-subtle)]">{item.meta}</p>}
|
|
25
|
-
</div>
|
|
26
|
-
<h3 class="mt-4 max-w-3xl text-2xl font-bold text-[color:var(--ts-color-text)] md:text-3xl">{item.title}</h3>
|
|
27
|
-
<p class="mt-3 max-w-3xl text-lg leading-9 text-[color:var(--ts-color-text-muted)]">{item.summary}</p>
|
|
28
|
-
{item.tags && item.tags.length > 0 && (
|
|
29
|
-
<p class="mt-4 text-sm uppercase tracking-[0.14em] text-[color:var(--ts-color-accent-strong)]">{item.tags.join(' / ')}</p>
|
|
30
|
-
)}
|
|
31
|
-
</a>
|
|
32
|
-
))}
|
|
33
|
-
</div>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<section class="relative overflow-hidden border-y border-[color:var(--ts-color-border-strong)] py-12 md:py-18">
|
|
2
|
-
<div class="absolute inset-y-0 left-[68%] hidden w-px bg-[linear-gradient(180deg,transparent,var(--ts-color-border-strong),transparent)] lg:block"></div>
|
|
3
|
-
<div class="grid gap-10 lg:grid-cols-[1.45fr_0.7fr] lg:items-start">
|
|
4
|
-
<div class="space-y-7 pr-0 lg:pr-10">
|
|
5
|
-
<slot name="eyebrow" />
|
|
6
|
-
<div class="space-y-4">
|
|
7
|
-
<slot name="title" />
|
|
8
|
-
<slot name="body" />
|
|
9
|
-
</div>
|
|
10
|
-
<div class="flex flex-wrap gap-4 pt-2">
|
|
11
|
-
<slot name="actions" />
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
<div class="border-l border-[color:var(--ts-color-border)] pl-0 lg:pl-8">
|
|
15
|
-
<slot name="aside" />
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
|
-
</section>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import ChronicleList from './ChronicleList.astro';
|
|
3
|
-
|
|
4
|
-
type NoteListEntry = {
|
|
5
|
-
id: string;
|
|
6
|
-
data: {
|
|
7
|
-
title: string;
|
|
8
|
-
summary: string;
|
|
9
|
-
status: 'live' | 'in progress' | 'exploratory' | 'planned' | 'speculative';
|
|
10
|
-
date: Date;
|
|
11
|
-
author?: string;
|
|
12
|
-
tags: string[];
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const { notes } = Astro.props as { notes: NoteListEntry[] };
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
<ChronicleList
|
|
20
|
-
items={notes.map((note) => ({
|
|
21
|
-
href: `/notes/${note.id}/`,
|
|
22
|
-
title: note.data.title,
|
|
23
|
-
summary: note.data.summary,
|
|
24
|
-
status: note.data.status,
|
|
25
|
-
date: note.data.date,
|
|
26
|
-
meta: note.data.author,
|
|
27
|
-
tags: note.data.tags,
|
|
28
|
-
}))}
|
|
29
|
-
/>
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
const { href, title, description, meta } = Astro.props;
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
<a href={href} class="group block border-t border-[color:var(--ts-color-border)] py-6 transition hover:border-[color:var(--ts-color-border-strong)]">
|
|
6
|
-
<div class="grid gap-3 md:grid-cols-[0.42fr_1fr_auto] md:items-start md:gap-6">
|
|
7
|
-
<div>
|
|
8
|
-
{meta && <p class="text-sm font-semibold uppercase tracking-[0.14em] text-[color:var(--ts-color-accent-strong)]">{meta}</p>}
|
|
9
|
-
</div>
|
|
10
|
-
<div>
|
|
11
|
-
<h3 class="text-2xl font-bold text-[color:var(--ts-color-text)]">{title}</h3>
|
|
12
|
-
<p class="mt-3 max-w-2xl text-base leading-8 text-[color:var(--ts-color-text-muted)]">{description}</p>
|
|
13
|
-
</div>
|
|
14
|
-
<p class="text-sm font-semibold uppercase tracking-[0.14em] text-[color:var(--ts-color-info-text)] group-hover:text-[color:var(--ts-color-accent-strong)]">Open path</p>
|
|
15
|
-
</div>
|
|
16
|
-
</a>
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import StatusBadge from '../content/StatusBadge.astro';
|
|
3
|
-
|
|
4
|
-
type ProfileItem = {
|
|
5
|
-
href: string;
|
|
6
|
-
name: string;
|
|
7
|
-
summary: string;
|
|
8
|
-
meta: string;
|
|
9
|
-
status?: 'live' | 'in progress' | 'exploratory' | 'planned' | 'speculative';
|
|
10
|
-
tags?: string[];
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const { items } = Astro.props as { items: ProfileItem[] };
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
<div class="grid gap-6 md:grid-cols-2">
|
|
17
|
-
{items.map((item) => (
|
|
18
|
-
<a href={item.href} class="border border-[color:var(--ts-color-border)] bg-[color:var(--ts-color-surface)] p-6 transition hover:border-[color:var(--ts-color-border-strong)]">
|
|
19
|
-
<div class="flex flex-wrap items-center gap-3">
|
|
20
|
-
{item.status && <StatusBadge status={item.status} />}
|
|
21
|
-
<p class="text-sm font-semibold uppercase tracking-[0.14em] text-[color:var(--ts-color-info-text)]">{item.meta}</p>
|
|
22
|
-
</div>
|
|
23
|
-
<h3 class="mt-4 text-2xl font-bold text-[color:var(--ts-color-text)]">{item.name}</h3>
|
|
24
|
-
<p class="mt-3 text-base leading-8 text-[color:var(--ts-color-text-muted)]">{item.summary}</p>
|
|
25
|
-
{item.tags && item.tags.length > 0 && (
|
|
26
|
-
<p class="mt-4 text-sm uppercase tracking-[0.14em] text-[color:var(--ts-color-accent-strong)]">{item.tags.join(' / ')}</p>
|
|
27
|
-
)}
|
|
28
|
-
</a>
|
|
29
|
-
))}
|
|
30
|
-
</div>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import MainLayout from '../../layouts/MainLayout.astro';
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
title = 'Page not found',
|
|
6
|
-
description = 'The requested Treeseed content could not be found.',
|
|
7
|
-
currentPath = '/404/',
|
|
8
|
-
} = Astro.props;
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
<MainLayout title={title} description={description} currentPath={currentPath}>
|
|
12
|
-
<section class="mx-auto max-w-3xl space-y-6 py-20">
|
|
13
|
-
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[color:var(--ts-color-accent-strong)]">404</p>
|
|
14
|
-
<h1 class="font-serif text-5xl text-[color:var(--ts-color-text)]">{title}</h1>
|
|
15
|
-
<p class="text-lg leading-9 text-[color:var(--ts-color-text-muted)]">{description}</p>
|
|
16
|
-
<div class="flex flex-wrap gap-4">
|
|
17
|
-
<a href="/" class="border border-[color:var(--ts-color-accent)] bg-[color:var(--ts-color-accent)] px-5 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">
|
|
18
|
-
Go home
|
|
19
|
-
</a>
|
|
20
|
-
<a href="/books/" class="border border-[color:var(--ts-color-border-strong)] px-5 py-3 text-base font-semibold text-[color:var(--ts-color-text)] transition hover:border-[color:var(--ts-color-info)] hover:bg-[color:var(--ts-color-info-soft)]">
|
|
21
|
-
Open books
|
|
22
|
-
</a>
|
|
23
|
-
</div>
|
|
24
|
-
</section>
|
|
25
|
-
</MainLayout>
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
const { eyebrow, title, description } = Astro.props;
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
<header class="max-w-4xl space-y-4">
|
|
6
|
-
{eyebrow && <p class="text-sm font-semibold uppercase tracking-[0.16em] text-[color:var(--ts-color-accent-strong)]">{eyebrow}</p>}
|
|
7
|
-
<h2 class="max-w-4xl font-serif text-4xl font-bold tracking-tight text-[color:var(--ts-color-text)] md:text-5xl">{title}</h2>
|
|
8
|
-
<p class="max-w-3xl text-lg leading-9 text-[color:var(--ts-color-text-muted)] md:text-xl">{description}</p>
|
|
9
|
-
</header>
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { PROJECT_STAGE } from '../../utils/content-status';
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
<div class="inline-flex max-w-full flex-wrap items-center gap-x-3 gap-y-1 border-l-4 border-[color:var(--ts-color-accent)] bg-[color:var(--ts-color-accent-soft)] px-4 py-3 text-[color:var(--ts-color-text)]">
|
|
6
|
-
<span class="font-bold uppercase tracking-[0.16em] text-xs">{PROJECT_STAGE.label}</span>
|
|
7
|
-
<span class="text-[color:var(--ts-color-text-muted)]">{PROJECT_STAGE.description}</span>
|
|
8
|
-
</div>
|