@treeseed/core 0.10.22 → 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.
Files changed (108) hide show
  1. package/README.md +69 -125
  2. package/dist/dev-watch.js +2 -1
  3. package/dist/dev.d.ts +1 -1
  4. package/dist/dev.js +51 -35
  5. package/dist/pages/404.astro +1 -1
  6. package/dist/pages/[slug].astro +4 -4
  7. package/dist/pages/agents/[slug].astro +3 -3
  8. package/dist/pages/agents/index.astro +3 -3
  9. package/dist/pages/books/[slug].astro +3 -3
  10. package/dist/pages/books/index.astro +3 -3
  11. package/dist/pages/contact.astro +2 -2
  12. package/dist/pages/decisions/[slug].astro +3 -3
  13. package/dist/pages/decisions/index.astro +3 -3
  14. package/dist/pages/docs-runtime/[...slug].astro +3 -3
  15. package/dist/pages/docs-runtime/index.astro +3 -3
  16. package/dist/pages/index.astro +11 -11
  17. package/dist/pages/notes/[slug].astro +3 -3
  18. package/dist/pages/notes/index.astro +3 -3
  19. package/dist/pages/objectives/[slug].astro +3 -3
  20. package/dist/pages/objectives/index.astro +3 -3
  21. package/dist/pages/people/[slug].astro +3 -3
  22. package/dist/pages/people/index.astro +3 -3
  23. package/dist/pages/proposals/[slug].astro +3 -3
  24. package/dist/pages/proposals/index.astro +3 -3
  25. package/dist/pages/questions/[slug].astro +3 -3
  26. package/dist/pages/questions/index.astro +3 -3
  27. package/dist/pages/ui/index.astro +23 -23
  28. package/dist/platform-resources.js +5 -1
  29. package/dist/scripts/build-dist.js +2 -0
  30. package/dist/scripts/release-verify.js +24 -2
  31. package/dist/scripts/workspace-bootstrap.js +3 -0
  32. package/dist/site.js +49 -11
  33. package/dist/styles/global.css +5 -5
  34. package/package.json +3 -45
  35. package/templates/github/deploy-web.workflow.yml +36 -2
  36. package/dist/components/DevWatchReload.astro +0 -45
  37. package/dist/components/SiteTitle.astro +0 -51
  38. package/dist/components/content/ContentStatusLegend.astro +0 -18
  39. package/dist/components/content/StatusBadge.astro +0 -11
  40. package/dist/components/docs/BookFontControls.astro +0 -180
  41. package/dist/components/docs/DesktopSidebarToggle.astro +0 -88
  42. package/dist/components/docs/DownloadBook.astro +0 -34
  43. package/dist/components/docs/Footer.astro +0 -112
  44. package/dist/components/docs/Header.astro +0 -157
  45. package/dist/components/docs/PageFrame.astro +0 -260
  46. package/dist/components/docs/PageSidebar.astro +0 -63
  47. package/dist/components/docs/PageTitle.astro +0 -39
  48. package/dist/components/docs/Sidebar.astro +0 -41
  49. package/dist/components/docs/ThemeSelect.astro +0 -5
  50. package/dist/components/forms/ContactForm.astro +0 -233
  51. package/dist/components/forms/FooterSubscribeForm.astro +0 -188
  52. package/dist/components/site/BookList.astro +0 -27
  53. package/dist/components/site/CTASection.astro +0 -24
  54. package/dist/components/site/ChronicleList.astro +0 -33
  55. package/dist/components/site/Hero.astro +0 -18
  56. package/dist/components/site/NotesList.astro +0 -29
  57. package/dist/components/site/PathCard.astro +0 -16
  58. package/dist/components/site/ProfileList.astro +0 -30
  59. package/dist/components/site/PublishedContentBody.astro +0 -5
  60. package/dist/components/site/RouteNotFound.astro +0 -25
  61. package/dist/components/site/SectionIntro.astro +0 -9
  62. package/dist/components/site/StageBanner.astro +0 -8
  63. package/dist/components/site/TrustCallout.astro +0 -9
  64. package/dist/components/starlight.js +0 -6
  65. package/dist/components/ui/data/ActionList.astro +0 -51
  66. package/dist/components/ui/data/Badge.astro +0 -19
  67. package/dist/components/ui/data/DataTable.astro +0 -51
  68. package/dist/components/ui/data/KeyValueList.astro +0 -28
  69. package/dist/components/ui/data/MetricCard.astro +0 -25
  70. package/dist/components/ui/data/MetricGrid.astro +0 -27
  71. package/dist/components/ui/data/StatusPill.astro +0 -20
  72. package/dist/components/ui/forms/Button.astro +0 -59
  73. package/dist/components/ui/forms/Field.astro +0 -39
  74. package/dist/components/ui/forms/FormActions.astro +0 -12
  75. package/dist/components/ui/forms/PasswordMeter.astro +0 -80
  76. package/dist/components/ui/forms/RadioGroup.astro +0 -55
  77. package/dist/components/ui/forms/Select.astro +0 -47
  78. package/dist/components/ui/forms/TextInput.astro +0 -58
  79. package/dist/components/ui/forms/Textarea.astro +0 -45
  80. package/dist/components/ui/layout/PageHeader.astro +0 -45
  81. package/dist/components/ui/shell/AppShell.astro +0 -130
  82. package/dist/components/ui/shell/BottomNav.astro +0 -42
  83. package/dist/components/ui/shell/ProjectHeader.astro +0 -66
  84. package/dist/components/ui/shell/PublicFooter.astro +0 -39
  85. package/dist/components/ui/shell/PublicShell.astro +0 -184
  86. package/dist/components/ui/shell/RailNav.astro +0 -42
  87. package/dist/components/ui/shell/ShellIconLink.astro +0 -30
  88. package/dist/components/ui/shell/TopBar.astro +0 -52
  89. package/dist/components/ui/surface/Card.astro +0 -46
  90. package/dist/components/ui/surface/EmptyState.astro +0 -45
  91. package/dist/components/ui/surface/Panel.astro +0 -54
  92. package/dist/components/ui/theme/ThemeMenu.astro +0 -58
  93. package/dist/components/ui/theme/ThemePreviewSwatch.astro +0 -18
  94. package/dist/components/ui/theme/ThemeScript.astro +0 -112
  95. package/dist/components/ui/theme/ThemeSelector.astro +0 -202
  96. package/dist/components/ui/types.js +0 -0
  97. package/dist/layouts/AuthoredEntryLayout.astro +0 -195
  98. package/dist/layouts/BookLayout.astro +0 -35
  99. package/dist/layouts/BridgeLayout.astro +0 -11
  100. package/dist/layouts/ContentLayout.astro +0 -24
  101. package/dist/layouts/MainLayout.astro +0 -76
  102. package/dist/layouts/NoteLayout.astro +0 -26
  103. package/dist/layouts/ProfileLayout.astro +0 -85
  104. package/dist/styles/app-shell.css +0 -626
  105. package/dist/styles/forms.css +0 -274
  106. package/dist/styles/theme.css +0 -198
  107. package/dist/styles/tokens.css +0 -65
  108. package/dist/styles/ui.css +0 -551
@@ -1,260 +0,0 @@
1
- ---
2
- import MobileMenuToggle from '../../vendor/starlight/components/MobileMenuToggle.astro';
3
- import { getBookForPath } from '../../utils/starlight-nav';
4
-
5
- const { hasSidebar, toc } = Astro.locals.starlightRoute;
6
- const hasToc = Boolean(toc);
7
- const isBookPage = Boolean(getBookForPath(Astro.url.pathname));
8
- ---
9
-
10
- <div class:list={['page sl-flex', isBookPage && 'page--book']}>
11
- <header class="header"><slot name="header" /></header>
12
- {
13
- hasSidebar && (
14
- <nav class="sidebar print:hidden" aria-label={Astro.locals.t('sidebarNav.accessibleLabel')}>
15
- <MobileMenuToggle />
16
- <div id="starlight__sidebar" class="sidebar-pane docs-left-sidebar">
17
- <div class="sidebar-content sl-flex">
18
- <slot name="sidebar" />
19
- </div>
20
- </div>
21
- </nav>
22
- )
23
- }
24
- <div class="main-frame"><slot /></div>
25
- <div class="docs-footer-host" data-docs-footer-host></div>
26
- </div>
27
-
28
- <script is:inline define:vars={{ hasSidebar, hasToc, isBookPage }}>
29
- const desktopQuery = window.matchMedia('(min-width: 72rem)');
30
- const storageKey = 'docs.desktop-sidebar-state';
31
- const bookFontScaleStorageKey = 'docs.book-font-scale';
32
- const defaultBookFontScale = 1;
33
- const minBookFontScale = 0.85;
34
- const maxBookFontScale = 1.35;
35
- const root = document.documentElement;
36
- const leftSidebar = document.getElementById('starlight__sidebar');
37
- const rightSidebar = document.getElementById('starlight__page-toc');
38
- const pageFrame = document.querySelector('.page');
39
- const footerHost = document.querySelector('[data-docs-footer-host]');
40
-
41
- const getDefaultState = () => ({
42
- left: !hasSidebar,
43
- right: !hasToc,
44
- });
45
-
46
- const readState = () => {
47
- try {
48
- const parsed = JSON.parse(localStorage.getItem(storageKey) || '{}');
49
- return {
50
- left: hasSidebar ? parsed.left === true : true,
51
- right: hasToc ? parsed.right === true : true,
52
- };
53
- } catch {
54
- return getDefaultState();
55
- }
56
- };
57
-
58
- const writeState = (state) => {
59
- try {
60
- localStorage.setItem(storageKey, JSON.stringify(state));
61
- } catch {
62
- // Ignore storage failures and keep the UI functional.
63
- }
64
- };
65
-
66
- const syncFooterOffset = () => {
67
- if (!(footerHost instanceof HTMLElement) || !desktopQuery.matches) {
68
- root.style.setProperty('--docs-footer-offset', '0px');
69
- return;
70
- }
71
-
72
- const footerTop = footerHost.getBoundingClientRect().top;
73
- const offset = Math.max(0, window.innerHeight - footerTop);
74
- root.style.setProperty('--docs-footer-offset', `${offset}px`);
75
- };
76
-
77
- const clampBookFontScale = (value) =>
78
- Math.min(maxBookFontScale, Math.max(minBookFontScale, value));
79
-
80
- const readBookFontScale = () => {
81
- try {
82
- const rawValue = localStorage.getItem(bookFontScaleStorageKey);
83
- const parsedValue = rawValue === null ? defaultBookFontScale : Number(rawValue);
84
- return Number.isFinite(parsedValue) ? clampBookFontScale(parsedValue) : defaultBookFontScale;
85
- } catch {
86
- return defaultBookFontScale;
87
- }
88
- };
89
-
90
- const applyBookFontState = () => {
91
- root.dataset.docsBookPage = String(isBookPage);
92
- if (!isBookPage) {
93
- root.style.removeProperty('--docs-book-font-scale');
94
- return;
95
- }
96
-
97
- root.style.setProperty('--docs-book-font-scale', String(readBookFontScale()));
98
- };
99
-
100
- const setExpanded = (side, expanded) => {
101
- root.dataset[`docs${side === 'left' ? 'Left' : 'Right'}SidebarExpanded`] = String(expanded);
102
- const toggle = document.querySelector(`[data-docs-sidebar-toggle="${side}"]`);
103
- if (toggle instanceof HTMLButtonElement) {
104
- toggle.setAttribute('aria-expanded', String(expanded));
105
- const label = toggle.dataset.label || '';
106
- toggle.setAttribute('aria-label', `${expanded ? 'Hide' : 'Show'} ${label}`);
107
- const text = toggle.querySelector('.desktop-sidebar-toggle__text');
108
- if (text) {
109
- text.textContent = `${expanded ? 'Hide' : 'Show'} ${label}`;
110
- }
111
- }
112
- };
113
-
114
- const syncAccessibility = () => {
115
- const desktop = desktopQuery.matches;
116
- const leftExpanded = root.dataset.docsLeftSidebarExpanded === 'true';
117
- const rightExpanded = root.dataset.docsRightSidebarExpanded === 'true';
118
-
119
- if (leftSidebar) {
120
- leftSidebar.toggleAttribute('inert', desktop && !leftExpanded);
121
- leftSidebar.setAttribute('aria-hidden', String(desktop && !leftExpanded));
122
- }
123
-
124
- if (rightSidebar) {
125
- rightSidebar.toggleAttribute('inert', desktop && !rightExpanded);
126
- rightSidebar.setAttribute('aria-hidden', String(desktop && !rightExpanded));
127
- }
128
- };
129
-
130
- const applyState = (state) => {
131
- setExpanded('left', hasSidebar ? state.left : false);
132
- setExpanded('right', hasToc ? state.right : false);
133
- syncAccessibility();
134
- syncFooterOffset();
135
- };
136
-
137
- const toggleSide = (side) => {
138
- const current =
139
- root.dataset[`docs${side === 'left' ? 'Left' : 'Right'}SidebarExpanded`] === 'true';
140
- const nextState = {
141
- left: root.dataset.docsLeftSidebarExpanded === 'true',
142
- right: root.dataset.docsRightSidebarExpanded === 'true',
143
- [side]: !current,
144
- };
145
- applyState(nextState);
146
- writeState(nextState);
147
- };
148
-
149
- const initialize = () => {
150
- applyBookFontState();
151
- const state = readState();
152
- applyState(state);
153
-
154
- if (pageFrame instanceof HTMLElement && footerHost instanceof HTMLElement) {
155
- const footerShell = pageFrame.querySelector('.docs-footer-shell');
156
- if (footerShell instanceof HTMLElement && footerShell.parentElement !== footerHost) {
157
- footerHost.append(footerShell);
158
- }
159
- }
160
-
161
- syncFooterOffset();
162
-
163
- for (const button of document.querySelectorAll('[data-docs-sidebar-toggle]')) {
164
- if (!(button instanceof HTMLButtonElement) || button.dataset.docsSidebarBound === 'true') continue;
165
- button.dataset.docsSidebarBound = 'true';
166
- button.addEventListener('click', () => {
167
- const side = button.dataset.docsSidebarToggle;
168
- if (side === 'left' || side === 'right') {
169
- toggleSide(side);
170
- }
171
- });
172
- }
173
- };
174
-
175
- initialize();
176
- desktopQuery.addEventListener('change', syncAccessibility);
177
- desktopQuery.addEventListener('change', syncFooterOffset);
178
- window.addEventListener('scroll', syncFooterOffset, { passive: true });
179
- window.addEventListener('resize', syncFooterOffset);
180
- </script>
181
-
182
- <style>
183
- @layer starlight.core {
184
- .page {
185
- flex-direction: column;
186
- min-height: 100vh;
187
- }
188
-
189
- .header {
190
- z-index: var(--sl-z-index-navbar);
191
- position: fixed;
192
- inset-inline-start: 0;
193
- inset-block-start: 0;
194
- width: 100%;
195
- height: var(--sl-nav-height);
196
- border-bottom: 1px solid var(--sl-color-hairline-shade);
197
- padding: var(--sl-nav-pad-y) var(--sl-nav-pad-x);
198
- padding-inline-end: var(--sl-nav-pad-x);
199
- background-color: var(--sl-color-bg-nav);
200
- }
201
-
202
- :global([data-has-sidebar]) .header {
203
- padding-inline-end: calc(
204
- var(--sl-nav-gap) + var(--sl-nav-pad-x) + var(--sl-menu-button-size)
205
- );
206
- }
207
-
208
- .sidebar-pane {
209
- visibility: var(--sl-sidebar-visibility, hidden);
210
- position: fixed;
211
- z-index: var(--sl-z-index-menu);
212
- inset-block: var(--sl-nav-height) var(--docs-footer-offset, 0px);
213
- inset-inline-start: 0;
214
- width: 100%;
215
- background-color: var(--sl-color-black);
216
- overflow-y: auto;
217
- }
218
-
219
- :global([aria-expanded='true']) ~ .sidebar-pane {
220
- --sl-sidebar-visibility: visible;
221
- }
222
-
223
- .sidebar-content {
224
- height: 100%;
225
- min-height: max-content;
226
- padding: 1rem var(--sl-sidebar-pad-x) 0;
227
- flex-direction: column;
228
- gap: 1rem;
229
- }
230
-
231
- @media (min-width: 50rem) {
232
- .sidebar-content::after {
233
- content: '';
234
- padding-bottom: 1px;
235
- }
236
- }
237
-
238
- .main-frame {
239
- padding-top: calc(var(--sl-nav-height) + var(--sl-mobile-toc-height));
240
- padding-inline-start: var(--sl-content-inline-start);
241
- }
242
-
243
- .docs-footer-host {
244
- width: 100%;
245
- }
246
-
247
- @media (min-width: 50rem) {
248
- :global([data-has-sidebar]) .header {
249
- padding-inline-end: var(--sl-nav-pad-x);
250
- }
251
- .sidebar-pane {
252
- --sl-sidebar-visibility: visible;
253
- width: var(--sl-sidebar-width);
254
- background-color: var(--sl-color-bg-sidebar);
255
- border-inline-end: 1px solid var(--sl-color-hairline-shade);
256
- overflow-y: auto;
257
- }
258
- }
259
- }
260
- </style>
@@ -1,63 +0,0 @@
1
- ---
2
- import MobileTableOfContents from '../../vendor/starlight/components/MobileTableOfContents.astro';
3
- import TableOfContents from '../../vendor/starlight/components/TableOfContents.astro';
4
- ---
5
-
6
- {
7
- Astro.locals.starlightRoute.toc && (
8
- <>
9
- <div class="lg:sl-hidden">
10
- <MobileTableOfContents />
11
- </div>
12
- <aside
13
- id="starlight__page-toc"
14
- class="right-sidebar-panel sl-hidden lg:sl-block"
15
- aria-label={Astro.locals.t('tableOfContents.onThisPage')}
16
- >
17
- <div class="sl-container">
18
- <TableOfContents />
19
- </div>
20
- </aside>
21
- </>
22
- )
23
- }
24
-
25
- <style>
26
- @layer starlight.core {
27
- .right-sidebar-panel {
28
- padding: 1rem var(--sl-sidebar-pad-x);
29
- }
30
- .sl-container {
31
- width: calc(var(--sl-sidebar-width) - 2 * var(--sl-sidebar-pad-x));
32
- }
33
- .right-sidebar-panel :global(h2) {
34
- color: var(--sl-color-white);
35
- font-size: var(--sl-text-h5);
36
- font-weight: 600;
37
- line-height: var(--sl-line-height-headings);
38
- margin-bottom: 0.5rem;
39
- }
40
- .right-sidebar-panel :global(:where(a)) {
41
- display: block;
42
- font-size: var(--sl-text-xs);
43
- text-decoration: none;
44
- color: var(--sl-color-gray-3);
45
- overflow-wrap: anywhere;
46
- }
47
- .right-sidebar-panel :global(:where(a):hover) {
48
- color: var(--sl-color-white);
49
- }
50
- @media (min-width: 72rem) {
51
- .sl-container {
52
- max-width: calc(
53
- (
54
- (
55
- 100vw - var(--sl-sidebar-width) - 2 * var(--sl-content-pad-x) - 2 *
56
- var(--sl-sidebar-pad-x)
57
- ) * 0.25
58
- )
59
- );
60
- }
61
- }
62
- }
63
- </style>
@@ -1,39 +0,0 @@
1
- ---
2
- import { getBookForPath } from '../../utils/starlight-nav';
3
-
4
- const isBookPage = Boolean(getBookForPath(Astro.url.pathname));
5
- const PAGE_TITLE_ID = '_top';
6
- ---
7
-
8
- <h1 id={PAGE_TITLE_ID} class:list={['docs-page-title', isBookPage && 'docs-page-title--book']}>
9
- {Astro.locals.starlightRoute.entry.data.title}
10
- </h1>
11
-
12
- <style>
13
- @layer starlight.core {
14
- .docs-page-title {
15
- margin-top: 1rem;
16
- font-size: var(--sl-text-h1);
17
- line-height: var(--sl-line-height-headings);
18
- font-weight: 600;
19
- color: var(--sl-color-white);
20
- }
21
-
22
- .docs-page-title--book {
23
- margin-top: 0;
24
- font-size: clamp(1rem, 1.1vw, 1.4rem);
25
- line-height: 1.18;
26
- font-weight: 800;
27
- color: var(--ts-color-accent-strong);
28
- text-wrap: pretty;
29
- }
30
-
31
- @media (min-width: 72rem) {
32
- .docs-page-title--book {
33
- max-width: var(--docs-book-panel-width);
34
- margin-inline: auto;
35
- text-wrap: balance;
36
- }
37
- }
38
- }
39
- </style>
@@ -1,41 +0,0 @@
1
- ---
2
- import MobileMenuFooter from '../../vendor/starlight/components/MobileMenuFooter.astro';
3
- import SidebarPersister from '../../vendor/starlight/components/SidebarPersister.astro';
4
- import SidebarSublist from '../../vendor/starlight/components/SidebarSublist.astro';
5
- import { TREESEED_LINKS, getBookForPath } from '../../utils/starlight-nav';
6
-
7
- const { sidebar } = Astro.locals.starlightRoute;
8
- const activeBook = getBookForPath(Astro.url.pathname);
9
- ---
10
-
11
- <SidebarPersister>
12
- {
13
- activeBook && (
14
- <div class="docs-sidebar-actions">
15
- <a href={TREESEED_LINKS.home} class="download-button docs-sidebar-books-button">
16
- All Books
17
- </a>
18
- </div>
19
- )
20
- }
21
- <SidebarSublist sublist={sidebar} />
22
- </SidebarPersister>
23
-
24
- <div class="md:sl-hidden">
25
- <MobileMenuFooter />
26
- </div>
27
-
28
- <style>
29
- @layer starlight.components {
30
- .docs-sidebar-actions {
31
- margin: 0 0 1.75rem;
32
- padding: 0 0.5rem;
33
- }
34
-
35
- .docs-sidebar-books-button {
36
- display: inline-flex;
37
- width: 100%;
38
- justify-content: center;
39
- }
40
- }
41
- </style>
@@ -1,5 +0,0 @@
1
- ---
2
- import ThemeScript from '../ui/theme/ThemeScript.astro';
3
- ---
4
-
5
- <ThemeScript includeCss={false} />
@@ -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>