A vibe coded tangled fork which supports pijul.
1{{ define "title" }}tangled · tightly-knit social coding{{ end }}
2
3{{ define "extrameta" }}
4 <!-- Open Graph Meta Tags -->
5 <meta property="og:title" content="tangled · tightly-knit social coding" />
6 <meta property="og:type" content="website" />
7 <meta property="og:url" content="https://tangled.org" />
8 <meta property="og:description" content="The next-generation social coding platform." />
9 <meta property="og:image" content="https://assets.tangled.network/tangled_og.png" />
10 <meta property="og:image:width" content="1200" />
11 <meta property="og:image:height" content="630" />
12
13 <!-- Twitter Card Meta Tags -->
14 <meta name="twitter:card" content="summary_large_image" />
15 <meta name="twitter:title" content="Tangled" />
16 <meta name="twitter:description" content="The next-generation social coding platform." />
17 <meta name="twitter:image" content="https://assets.tangled.network/tangled_og.png" />
18
19 <!-- Additional SEO -->
20 <meta name="description" content="The next-generation social coding platform. Host repos on your infrastructure with knots, use stacked pull requests, and run CI with spindles." />
21 <link rel="canonical" href="https://tangled.org" />
22{{ end }}
23
24
25{{ define "content" }}
26 <div class="flex flex-col gap-24 md:gap-40 my-24 md:my-32">
27 {{ template "timeline/fragments/hero" . }}
28 {{ template "timeline/fragments/preview" . }}
29 {{ template "features1" . }}
30 {{ template "features2" . }}
31 {{ template "recentUpdates" . }}
32 {{ template "community" . }}
33 </div>
34{{ end }}
35
36{{ block "topbarLayout" . }}
37 <header class="max-w-screen-xl mx-auto w-full col-span-full md:col-span-1 md:col-start-2" style="z-index: 20;">
38 {{ if .LoggedInUser }}
39 <div id="upgrade-banner"
40 hx-get="/upgradeBanner"
41 hx-trigger="load"
42 hx-swap="innerHTML">
43 </div>
44 {{ end }}
45 {{ template "layouts/fragments/topbar" . }}
46 </header>
47{{ end }}
48
49{{ block "footerLayout" . }}
50 <footer class="z-10">
51 {{ template "layouts/fragments/footer" . }}
52 </footer>
53{{ end }}
54
55{{ block "bodyClasses" . }}
56 bg-transparent bg-gradient-to-b from-white to-slate-100 dark:bg-none dark:bg-gray-900
57{{ end }}
58
59{{ block "mainLayout" . }}
60 <div class="flex-grow relative">
61 <div
62 class="absolute opacity-50 dark:opacity-5 inset-x-0 top-0 bottom-[-50px] md:bottom-[-150px] pointer-events-none bg-[url('https://assets.tangled.network/yarn_ball.svg')] bg-no-repeat bg-right-bottom bg-[length:500px] md:bg-[length:1000px] w-full">
63 </div>
64 <div class="max-w-screen-xl mx-auto flex flex-col gap-4 relative z-10">
65 {{ block "contentLayout" . }}
66 <main>
67 {{ block "content" . }}{{ end }}
68 </main>
69 {{ end }}
70
71 {{ block "contentAfterLayout" . }}
72 <main>
73 {{ block "contentAfter" . }}{{ end }}
74 </main>
75 {{ end }}
76 </div>
77 </div>
78{{ end }}
79
80{{ define "features1" }}
81 {{ $labelStyle := "normal-case cursor-pointer w-auto md:w-full p-4 md:px-6 rounded bg-white dark:bg-gray-800 font-medium text-base md:text-lg opacity-50 border border-gray-200 dark:border-gray-700 relative overflow-hidden" }}
82 {{ $spanStyle := "z-10 items-center justify-between gap-2 w-full" }}
83 {{ $connectorStyle := "w-0.5 h-6 bg-gray-300 dark:bg-gray-600 opacity-0 mx-auto" }}
84 {{ $contentStyle := "hidden bg-white dark:bg-gray-800 rounded shadow-sm p-6 border border-gray-200 dark:border-gray-700 grid-cols-1 md:grid-cols-2" }}
85 {{ $progressOverlayStyle := "absolute inset-0 bg-gray-600/10 dark:bg-gray-100/10 w-0 transition-none" }}
86
87 <style>
88 @media (max-width: 768px) {
89 .features-grid:has(#feature-prs:checked) {
90 grid-template-columns: 1fr auto auto;
91 }
92
93 .features-grid:has(#feature-knots:checked) {
94 grid-template-columns: auto 1fr auto;
95 }
96
97 .features-grid:has(#feature-spindles:checked) {
98 grid-template-columns: auto auto 1fr;
99 }
100
101 #feature-prs:checked ~ label[for="feature-prs"] .label-text,
102 #feature-knots:checked ~ label[for="feature-knots"] .label-text,
103 #feature-spindles:checked ~ label[for="feature-spindles"] .label-text {
104 display: inline-flex !important;
105 }
106 #feature-prs:checked ~ label[for="feature-prs"] .icon-only,
107 #feature-knots:checked ~ label[for="feature-knots"] .icon-only,
108 #feature-spindles:checked ~ label[for="feature-spindles"] .icon-only {
109 display: none !important;
110 }
111 }
112 </style>
113
114 <div class="features-grid w-full grid grid-cols-3 gap-x-6 px-2">
115 <input type="radio" id="feature-prs" name="feature" class="peer/prs hidden" checked />
116 <input type="radio" id="feature-knots" name="feature" class="peer/knots hidden" />
117 <input type="radio" id="feature-spindles" name="feature" class="peer/spindles hidden" />
118
119 <label for="feature-prs" class="{{ $labelStyle }} peer-checked/prs:opacity-100 peer-checked/prs:shadow-sm">
120 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">A better way to review {{ i "git-pull-request" "size-5" }}</span>
121 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "git-pull-request" "size-5" }}</span>
122 <div class="{{ $progressOverlayStyle }}" data-progress="prs"></div>
123 </label>
124
125 <label for="feature-knots" class="{{ $labelStyle }} peer-checked/knots:opacity-100 peer-checked/knots:shadow-sm">
126 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">Completely self-hostable {{ i "hard-drive" "size-5" }}</span>
127 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "hard-drive" "size-5" }}</span>
128 <div class="{{ $progressOverlayStyle }}" data-progress="knots"></div>
129 </label>
130
131 <label for="feature-spindles" class="{{ $labelStyle }} peer-checked/spindles:opacity-100 peer-checked/spindles:shadow-sm">
132 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">Quick and easy CI {{ i "layers-2" "size-5" }}</span>
133 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "layers-2" "size-5" }}</span>
134 <div class="{{ $progressOverlayStyle }}" data-progress="spindles"></div>
135 </label>
136
137 <div class="{{ $connectorStyle }} peer-checked/prs:opacity-100"></div>
138 <div class="{{ $connectorStyle }} peer-checked/knots:opacity-100"></div>
139 <div class="{{ $connectorStyle }} peer-checked/spindles:opacity-100"></div>
140
141 {{ $titleStyle := "text-xl md:text-2xl font-bold my-2 text-gray-900 dark:text-gray-100" }}
142 {{ $textContentStyle := "pb-6 md:p-6 md:pr-12 md:text-lg" }}
143 {{ $imgContentStyle := "w-full p-6 min-h-96 min-h-[400px] md:min-h-[500px] rounded overflow-hidden place-content-center bg-gradient-to-b from-slate-50 to-slate-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700" }}
144
145 <div class="col-span-3 {{ $contentStyle }} peer-checked/prs:grid ">
146 <div class="{{ $textContentStyle }} flex flex-col gap-4 md:gap-8 place-self-center">
147 <section>
148 <h1 class="{{ $titleStyle }}">Stacked PRs</h1>
149 <p>
150 Break down large features into small, reviewable chunks. Stack pull
151 requests on top of each other and ship faster with better code
152 reviews. Tangled natively supports stacking using Jujutsu <a
153 href="https://docs.jj-vcs.dev/latest/glossary/#change-id"
154 class="underline">Change-Ids</a>.
155 </p>
156 </section>
157 <section>
158 <h1 class="{{ $titleStyle }}">Round-based review</h1>
159 <p>
160 Pull-requests in Tangled are round-based, which means, each
161 submission is "immutable". To update a PR, the author must start a
162 second revision or "round". As a reviewer, you never have to worry
163 about a PR changing <em>during</em> a review!
164 </p>
165 </section>
166 </div>
167 <div class="{{ $imgContentStyle }} pl-24 md:pl-40">
168 <div class="flex flex-col" style="transform: perspective(1000px) rotateX(15deg)">
169 {{ $mockPRs := list
170 (dict "Id" 941 "Title" "spindle/nixery: update setup command docs" "State" "open" )
171 (dict "Id" 940 "Title" "appview/pulls: fix search not updating count of pull requests" "State" "open" )
172 (dict "Id" 939 "Title" "appview/pages: improved seo tags for home, repo and profile" "State" "open" )
173 (dict "Id" 938 "Title" "appview/state: update robots.txt" "State" "merged" )
174 (dict "Id" 937 "Title" "appview/profile: show dummy profile when no tangled profile" "State" "merged" )
175 (dict "Id" 936 "Title" "appview/pipelines: fix incorrect totals" "State" "merged" )
176 }}
177 {{ range $index, $pr := $mockPRs }}
178 {{ $scale := 1.4 }}
179 {{ if gt $index 0 }}
180 {{ $scale = subf64 1.4 (mulf64 (f64 $index) 0.04) }}
181 {{ if lt $scale 1.0 }}{{ $scale = 1.0 }}{{ end }}
182 {{ end }}
183 {{ $zIndex := sub 100 $index }}
184 {{ template "mockPRRow" (dict "PR" $pr "Scale" $scale "ZIndex" $zIndex) }}
185 {{ end }}
186 </div>
187 </div>
188 </div>
189
190 <div class="col-span-3 {{ $contentStyle }} peer-checked/knots:grid">
191 <div class="{{ $textContentStyle }} flex flex-col gap-4 md:gap-8 place-self-center">
192 <section>
193 <h1 class="{{ $titleStyle }}">Knots</h1>
194 <p>
195 Host your repositories on your own infrastructure with <a
196 href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide" class="underline">knots</a>.
197 Knots are lightweight git repository hosts that syndicate git
198 operations across the network. You can setup a knot server on a
199 machine as small as a Raspberry Pi!
200 <br>
201 <br>
202 If you want to try Tangled without self-hosting, fear not! All users
203 are added to our hosted knot by default.
204 </p>
205 </section>
206 <section>
207 <h1 class="{{ $titleStyle }}">Spindles</h1>
208 <p>
209 Host CI runners on your own infrastructure with <a
210 href="https://docs.tangled.org/spindles.html#self-hosting-guide" class="underline">spindles</a>.
211 Spindles are responsible for queuing up CI jobs and syndicating
212 pipeline statuses to the network. Presently, the task of sandboxing
213 workflows and caching dependencies is delegated to Docker.
214 </p>
215 </section>
216 </div>
217 {{ $planetWrapperStyle := "absolute left-1/2 bottom-0 flex items-start justify-center -translate-x-1/2 translate-y-1/2" }}
218 {{ $orbitStyle := "absolute left-1/2 bottom-0 rounded-full border-2 border-gray-200 dark:border-gray-700 -translate-x-1/2 translate-y-1/2 shadow-lg" }}
219 {{ $planetStyle := "-mt-8 size-16 md:-mt-10 md:size-20 rounded-full shadow-sm border flex items-center justify-center" }}
220 <div class="{{ $imgContentStyle }} min-h-[500px] relative">
221 <div class="size-[250px] {{ $orbitStyle }}"></div>
222 <div class="size-[500px] {{ $orbitStyle }}"></div>
223 <div class="size-[750px] {{ $orbitStyle }}"></div>
224
225 <!-- planets on inner orbit -->
226 <div class="{{ $planetWrapperStyle }} size-[250px] rotate-[12deg]">
227 <div class="{{ $planetStyle }} -rotate-[12deg] bg-blue-100 dark:bg-blue-900 border-blue-500 dark:border-blue-500">
228 {{ i "server" "size-8" "text-blue-500" "dark:text-blue-500" }}
229 </div>
230 </div>
231
232 <!-- Planets on middle orbit -->
233 <div class="{{ $planetWrapperStyle }} size-[500px] -rotate-[32deg]">
234 <div class="{{ $planetStyle }} rotate-[32deg] bg-green-100 dark:bg-green-900 border-green-500 dark:border-green-500">
235 {{ i "hard-drive" "size-8" "text-green-500" "dark:text-green-500" }}
236 </div>
237 </div>
238 <div class="{{ $planetWrapperStyle }} size-[500px] rotate-[24deg]">
239 <div class="{{ $planetStyle }} -rotate-[24deg] bg-amber-100 dark:bg-amber-900 border-amber-500 dark:border-amber-500">
240 {{ i "server" "size-8" "text-amber-500" "dark:text-amber-500" }}
241 </div>
242 </div>
243
244 <!-- Planets on outer orbit -->
245 <div class="{{ $planetWrapperStyle }} size-[750px] -rotate-[40deg]">
246 <div class="{{ $planetStyle }} rotate-[40deg] bg-blue-100 dark:bg-blue-900 border-blue-500 dark:border-blue-500">
247 {{ i "server" "size-8" "text-blue-500" "dark:text-blue-500" }}
248 </div>
249 </div>
250 <div class="{{ $planetWrapperStyle }} size-[750px] rotate-[3deg]">
251 <div class="{{ $planetStyle }} -rotate-[3deg] bg-green-100 dark:bg-green-900 border-green-500 dark:border-green-500">
252 {{ i "router" "size-8" "text-green-500" "dark:text-green-500" }}
253 </div>
254 </div>
255 <div class="{{ $planetWrapperStyle }} size-[750px] rotate-[44deg]">
256 <div class="{{ $planetStyle }} -rotate-[44deg] bg-amber-100 dark:bg-amber-900 border-amber-500 dark:border-amber-500">
257 {{ i "database" "size-8" "text-amber-500" "dark:text-amber-500" }}
258 </div>
259 </div>
260 </div>
261 </div>
262
263 <div class="col-span-3 {{ $contentStyle }} peer-checked/spindles:grid">
264 <div class="{{ $textContentStyle }} flex flex-col gap-4 md:gap-8 place-self-center">
265 <section>
266 <h1 class="{{ $titleStyle }}">Nix-powered CI</h1>
267 <p>
268 Pick and choose dependencies for your CI pipelines from <a
269 href="https://docs.tangled.org/spindles.html#dependencies"
270 class="underline"><code>nixpkgs</code></a>, one of the biggest
271 package repositories.
272 <br>
273 <br>
274 All dependencies and workflow images are cached using <a
275 href="https://nixery.dev/">nixery</a>. Subsuquent runs of your CI
276 pipeline will load all dependencies almost instantly.
277 </p>
278 </section>
279 <section>
280 <div class="flex items-center gap-2 my-4">
281 <h1 class="{{ $titleStyle }}">Customizable Engines</h1>
282 <span class="text-sm text-blue-500 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/50 rounded p-1">coming soon</span>
283 </div>
284 <p>
285 We know nix is not for everybody! Spindles are built to have
286 swappable engines, on our roadmap are Docker and Firecracker based
287 engines.
288 </p>
289 </section>
290 </div>
291 <div class="{{ $imgContentStyle }} flex items-end pb-0">
292 <div class="w-full mx-auto border-t border-l border-r border-gray-200 dark:border-gray-700 rounded-t overflow-hidden shadow-lg">
293 <div class="bg-white dark:bg-gray-800 px-4 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center gap-2 justify-between">
294 <span class="font-mono text-gray-600 dark:text-gray-400 text-sm md:text-lg">.tangled/workflows/test.yml</span>
295 <div class="flex items-center gap-2">
296 <div class="size-3 rounded-full bg-red-500"></div>
297 <div class="size-3 rounded-full bg-yellow-500"></div>
298 <div class="size-3 rounded-full bg-green-500"></div>
299 </div>
300 </div>
301 <div class="bg-white dark:bg-gray-800 p-4 overflow-x-auto text-sm md:text-lg">
302 {{ $yamlContent := `when:
303 - event: push
304 branch: main
305
306dependencies:
307 nixpkgs:
308 - go
309 - gcc
310
311environment:
312 CGO_ENABLED: 1
313
314steps:
315 - name: run all tests
316 command: go test -v ./...` }}
317 <div class="whitespace-pre">{{ code $yamlContent ".tangled/workflows/test.yml" | escapeHtml }}</div>
318 </div>
319 </div>
320 </div>
321 </div>
322 </div>
323
324 <script>
325 document.addEventListener('DOMContentLoaded', function() {
326 const featureIds = ['feature-prs', 'feature-knots', 'feature-spindles'];
327 const progressNames = ['prs', 'knots', 'spindles'];
328 let currentIndex = 0;
329 let timerInterval = null;
330
331 function stopTimer() {
332 if (timerInterval) {
333 clearInterval(timerInterval);
334 timerInterval = null;
335 }
336 document.querySelectorAll('[data-progress]').forEach(el => {
337 el.classList.remove('animate-progress');
338 });
339 }
340
341 const firstProgress = document.querySelector('[data-progress="prs"]');
342 if (firstProgress) {
343 firstProgress.classList.add('animate-progress');
344 }
345
346 document.querySelectorAll('label[for^="feature-"]').forEach(label => {
347 label.addEventListener('click', stopTimer);
348 });
349
350 timerInterval = setInterval(function() {
351 document.querySelectorAll('[data-progress]').forEach(el => {
352 el.classList.remove('animate-progress');
353 void el.offsetWidth; // force reflow
354 });
355
356 currentIndex = (currentIndex + 1) % featureIds.length;
357 document.getElementById(featureIds[currentIndex]).checked = true;
358
359 const activeProgress = document.querySelector('[data-progress="' + progressNames[currentIndex] + '"]');
360 if (activeProgress) {
361 activeProgress.classList.add('animate-progress');
362 }
363 }, 5000);
364 });
365 </script>
366{{ end }}
367
368{{ define "mockPRRow" }}
369 {{ $pr := .PR }}
370 <div class="flex text-sm items-center justify-between w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3 shadow relative" style="transform: scale({{ .Scale }}); transform-origin: top center; z-index: {{ .ZIndex }};">
371 <div class="flex items-center gap-2 min-w-0 flex-1 pr-2">
372 <div class="flex-shrink-0">
373 {{ if eq $pr.State "open" }}
374 {{ i "git-pull-request" "size-4 text-green-500" }}
375 {{ else }}
376 {{ i "git-merge" "size-4 text-purple-500" }}
377 {{ end }}
378 </div>
379 <span class="truncate text-sm text-gray-800 dark:text-gray-200">
380 <span class="text-gray-500 dark:text-gray-400">#{{ $pr.Id }}</span>
381 {{ $pr.Title }}
382 </span>
383 </div>
384 </div>
385{{ end }}
386
387{{ define "features2" }}
388 {{ $cardStyle := "bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700" }}
389 {{ $cardInnerStyle := "flex flex-row items-center gap-4 md:gap-6 p-4 md:p-6 md:pt-8 relative" }}
390 {{ $contentStyle := "flex-1 flex flex-col" }}
391 {{ $titleStyle := "text-xl md:text-2xl font-bold mb-2 md:mb-3 text-gray-900 dark:text-gray-100" }}
392 {{ $descriptionStyle := "text-gray-600 dark:text-gray-300 text-sm md:text-base leading-relaxed mb-3 md:mb-0" }}
393 {{ $linkMobileStyle := "hover:no-underline inline-flex md:hidden items-center gap-2 text-sm text-gray-700 dark:text-gray-300 font-medium" }}
394 {{ $iconContainerStyle := "relative shrink-0 w-24 h-24 md:w-48 md:h-48" }}
395 {{ $iconCircleStyle := "w-full h-full rounded-full flex items-center justify-center" }}
396 {{ $linkDesktopStyle := "hover:no-underline hidden md:inline-flex absolute -bottom-10 -right-14 items-center gap-2 p-3 text-base btn" }}
397
398 <div class="w-full flex flex-col gap-6 md:gap-40 max-w-5xl mx-auto px-2">
399 <div class="{{ $cardStyle }} md:mr-32">
400 <div class="{{ $cardInnerStyle }}">
401 <div class="{{ $contentStyle }}">
402 <h3 class="{{ $titleStyle }}">Built on AT Protocol</h3>
403 <p class="{{ $descriptionStyle }}">
404 AT Protocol enables federated code-collaboration. Submit
405 pull-requests or bug-reports to any repository hosted on any
406 server.
407 <br>
408 <br>
409 Bring an existing AT Protocol account (such as your Bluesky
410 account), or signup for one with us.
411 <a class="underline" href="https://docs.tangled.org/quick-start-guide.html#login-or-sign-up">Read the docs to know more.</a>
412 </p>
413 <a href="TODO" class="{{ $linkMobileStyle }}">
414 Learn more
415 {{ i "arrow-right" "size-4" }}
416 </a>
417 </div>
418 <div class="{{ $iconContainerStyle }}">
419 <div class="{{ $iconCircleStyle }} bg-blue-100 dark:bg-blue-900/50">
420 {{ i "at-sign" "size-12 md:size-24" "text-blue-500 dark:text-blue-500" "rotate-12" }}
421 </div>
422 <a href="TODO" class="{{ $linkDesktopStyle }}">
423 Learn more
424 {{ i "arrow-right" "size-4" }}
425 </a>
426 </div>
427 </div>
428 </div>
429
430 <div class="{{ $cardStyle }} md:ml-32">
431 <div class="{{ $cardInnerStyle }}">
432 <div class="{{ $contentStyle }}">
433 <h3 class="{{ $titleStyle }}">Free and Open-source</h3>
434 <p class="{{ $descriptionStyle }}">
435 All of Tangled is open-source and built in the open!
436 Check out the <a class="underline" href="https://tangled.org/core">monorepo</a> and join in on the fun.
437 <br>
438 <br>
439 We welcome contributions however big or small. You can start contributing by picking up a
440 <a class="underline" href="https://tangled.org/tangled.org/core/issues">good-first-issue</a>.
441 </p>
442 <a href="TODO" class="{{ $linkMobileStyle }}">
443 Learn more
444 {{ i "arrow-right" "size-4" }}
445 </a>
446 </div>
447 <div class="{{ $iconContainerStyle }}">
448 <div class="{{ $iconCircleStyle }} bg-green-100 dark:bg-green-900/50">
449 {{ i "package-open" "size-12 md:size-24" "text-green-500 dark:text-green-500" "-rotate-12" }}
450 </div>
451 <a href="TODO" class="{{ $linkDesktopStyle }}">
452 Learn more
453 {{ i "arrow-right" "size-4" }}
454 </a>
455 </div>
456 </div>
457 </div>
458
459 <div class="{{ $cardStyle }} md:mr-32">
460 <div class="{{ $cardInnerStyle }}">
461 <div class="{{ $contentStyle }}">
462 <h3 class="{{ $titleStyle }}">Social coding is back</h3>
463 <p class="{{ $descriptionStyle }}">
464 Discover trending projects, follow your friends and star your favourite repositories. Coding is better together!
465 <br>
466 <br>
467 You can use one account for all of the atmosphere. If you have
468 friends on Bluesky, you will find them on Tangled with the same
469 handle.
470 </p>
471 <a href="TODO" class="{{ $linkMobileStyle }}">
472 Learn more
473 {{ i "arrow-right" "size-4" }}
474 </a>
475 </div>
476 <div class="{{ $iconContainerStyle }}">
477 <div class="{{ $iconCircleStyle }} bg-amber-100 dark:bg-amber-900/50">
478 {{ i "message-circle-heart" "size-12 md:size-24" "text-amber-500 dark:text-amber-500" "rotate-12" }}
479 </div>
480 <a href="TODO" class="{{ $linkDesktopStyle }}">
481 Learn more
482 {{ i "arrow-right" "size-4" }}
483 </a>
484 </div>
485 </div>
486 </div>
487 </div>
488{{ end }}
489
490{{ define "changelog" }}
491 <div class="w-full px-2 py-16 md:py-24">
492 <h2 class="text-3xl md:text-4xl font-bold mb-8 text-gray-900 dark:text-gray-100">Changelog</h2>
493
494 <div class="grid grid-cols-1 gap-6 mb-6">
495 {{ range $index := list 0 1 2 3 }}
496 <div class="bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 p-6">
497 <div class="flex items-center gap-2 mb-3">
498 <span class="text-xs font-medium text-gray-500 dark:text-gray-400">v1.{{ sub 3 $index }}.0</span>
499 <span class="text-xs text-gray-400 dark:text-gray-500">•</span>
500 <span class="text-xs text-gray-500 dark:text-gray-400">Feb {{ sub 16 $index }}, 2026</span>
501 </div>
502 <h3 class="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100">Feature Update {{ sub 4 $index }}</h3>
503 <p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
504 Improvements to the platform including bug fixes, performance enhancements, and new features.
505 </p>
506 </div>
507 {{ end }}
508 </div>
509
510 <div class="flex justify-end">
511 <a href="/changelog" class="hover:no-underline inline-flex items-center gap-2 px-4 py-2 text-base btn">
512 View full changelog
513 {{ i "arrow-right" "size-4" }}
514 </a>
515 </div>
516 </div>
517{{ end }}
518
519{{ define "recentUpdates" }}
520 <div class="w-full px-2 py-16 md:py-24">
521 <h2 class="px-4 text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2">Recent updates</h2>
522 <p class="px-4 mb-8 text-gray-500 dark:text-gray-400">
523 Follow <a href="https://bsky.app/profile/tangled.org">@tangled.org</a> on Bluesky for more!
524 </p>
525 <div class="columns-1 md:columns-2 lg:columns-3 gap-6">
526 {{ range $index, $post := .BlueskyPosts }}
527 <div class="{{ if ge $index 3 }}hidden md:block{{ end }}">
528 {{ template "post" $post }}
529 </div>
530 {{ end }}
531 </div>
532 </div>
533{{ end }}
534
535{{ define "post" }}
536 <div class="bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 px-6 py-4 flex flex-col gap-2 break-inside-avoid mb-6">
537 <div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
538 <span class="flex items-center gap-2">
539 {{ template "user/fragments/picHandle" "tangled.org" }}
540 </span>
541 <span>{{ template "repo/fragments/shortTimeAgo" .CreatedAt }}</span>
542 </div>
543 <p class="text-gray-900 dark:text-gray-100 text-base leading-relaxed whitespace-pre-wrap">{{ .Text }}</p>
544
545 {{ if .Embed }}
546 {{ if .Embed.EmbedImages_View }}
547 <div class="grid {{ if eq (len .Embed.EmbedImages_View.Images) 1 }}grid-cols-1{{ else if eq (len .Embed.EmbedImages_View.Images) 2 }}grid-cols-2{{ else }}grid-cols-2{{ end }} gap-2">
548 {{ range .Embed.EmbedImages_View.Images }}
549 <img src="{{ .Fullsize }}" alt="{{ .Alt }}" class="rounded w-full h-auto object-cover border border-gray-200 dark:border-gray-700" loading="lazy" />
550 {{ end }}
551 </div>
552 {{ else if .Embed.EmbedExternal_View }}
553 <a href="{{ .Embed.EmbedExternal_View.External.Uri }}" target="_blank" rel="noopener noreferrer" class="hover:no-underline block border border-gray-200 dark:border-gray-700 rounded overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
554 {{ if .Embed.EmbedExternal_View.External.Thumb }}
555 <img src="{{ .Embed.EmbedExternal_View.External.Thumb }}" alt="" class="w-full h-48 object-cover" loading="lazy" />
556 {{ end }}
557 <div class="p-3">
558 <div class="font-medium text-gray-900 dark:text-gray-100 text-sm mb-1">{{ .Embed.EmbedExternal_View.External.Title }}</div>
559 <div class="text-xs text-gray-600 dark:text-gray-400 line-clamp-2">{{ .Embed.EmbedExternal_View.External.Description }}</div>
560 <div class="text-xs text-gray-500 dark:text-gray-500 mt-1">{{ .Embed.EmbedExternal_View.External.Uri }}</div>
561 </div>
562 </a>
563 {{ else if .Embed.EmbedVideo_View }}
564 <div class="rounded overflow-hidden bg-gray-100 dark:bg-gray-700 aspect-video flex items-center justify-center">
565 <span class="text-gray-500 dark:text-gray-400 text-sm">Video embed</span>
566 </div>
567 {{ end }}
568 {{ end }}
569
570 <a href="https://bsky.app/profile/tangled.org/post/{{ .Rkey }}"
571 target="_blank"
572 rel="noopener noreferrer"
573 class="flex items-center justify-between gap-4 text-sm text-gray-500 dark:text-gray-400 pt-2 no-underline hover:no-underline">
574 <div class="flex items-center gap-4">
575 <div class="flex items-center gap-1">
576 {{ i "heart" "size-4" }}
577 <span>{{ .LikeCount }}</span>
578 </div>
579 <div class="flex items-center gap-1">
580 {{ i "repeat-2" "size-4" }}
581 <span>{{ .RepostCount }}</span>
582 </div>
583 <div class="flex items-center gap-1">
584 {{ i "reply" "size-4 -scale-x-1" }}
585 <span>{{ add64 .ReplyCount .QuoteCount }}</span>
586 </div>
587 </div>
588 {{ i "arrow-up-right" "size-4" }}
589 </a>
590 </div>
591{{ end }}
592
593{{ define "community" }}
594 <div class="w-full px-2 py-16 md:py-24">
595 <div class="max-w-2xl mx-auto text-center space-y-6">
596 <h2 class="text-3xl md:text-6xl font-bold text-gray-900 dark:text-gray-100">Join the network</h2>
597 <p class="text-xl text-gray-600 dark:text-gray-300">
598 You can participate in the Tangled network with an AT account. If you
599 don't know what that is, you can sign up for one below.
600 <br>
601 <a href="https://docs.tangled.org/quick-start-guide.html#login-or-sign-up">Read more on the docs.</a>
602 </p>
603 <form class="flex gap-2 items-stretch w-full md:max-w-md mx-auto p-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 rounded shadow-sm">
604 <input
605 type="email"
606 id="email"
607 name="email"
608 tabindex="4"
609 required
610 placeholder="Enter your email"
611 class="py-2 w-full"
612 />
613 <button class="btn-create flex items-center gap-2 text-base whitespace-nowrap" type="submit">
614 join now
615 {{ i "arrow-right" "size-4" }}
616 </button>
617 </form>
618 </div>
619{{ end }}