This repository has no description
1{{ define "title" }}Tangled · The next-generation social coding platform{{ end }}
2
3{{ define "extrameta" }}
4 <!-- Open Graph Meta Tags -->
5 <meta property="og:title" content="Tangled · The next-generation social coding platform" />
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 repositories on your infrastructure with knots, use stacked pull requests, and run CI with spindles." />
21 <link rel="canonical" href="https://tangled.org" />
22
23 <script>
24 document.addEventListener('DOMContentLoaded', () => {
25 const videos = document.querySelectorAll('video[data-hls]');
26 if (!videos.length) return;
27
28 const attach = () => {
29 videos.forEach((video) => {
30 const src = video.dataset.hls;
31 if (video.canPlayType('application/vnd.apple.mpegurl')) {
32 video.src = src;
33 } else if (window.Hls && Hls.isSupported()) {
34 const hls = new Hls();
35 hls.loadSource(src);
36 hls.attachMedia(video);
37 }
38 });
39 };
40
41 const needsHls = Array.from(videos).some(
42 (v) => !v.canPlayType('application/vnd.apple.mpegurl')
43 );
44 if (!needsHls) {
45 attach();
46 return;
47 }
48
49 const script = document.createElement('script');
50 script.src = '/static/hls.min.js';
51 script.onload = attach;
52 document.head.appendChild(script);
53 });
54 </script>
55 <script>
56 document.addEventListener('DOMContentLoaded', () => {
57 document.querySelectorAll('[data-bsky-carousel]').forEach(el => {
58 let maxRatio = 0;
59 el.querySelectorAll('img[data-aspect]').forEach(img => {
60 const [w, h] = img.dataset.aspect.split(':').map(Number);
61 if (h > 0) maxRatio = Math.max(maxRatio, w / h);
62 });
63 if (maxRatio > 0) {
64 el.style.height = Math.min(Math.round(el.clientWidth / maxRatio), 400) + 'px';
65 }
66 });
67 });
68 </script>
69{{ end }}
70
71
72{{ define "content" }}
73 <div class="flex flex-col gap-20 md:gap-40 my-20 md:my-32">
74 <div class="relative">
75 <div class="hidden md:block absolute inset-0 pointer-events-none z-0 opacity-50 dark:opacity-5
76 [mask-image:linear-gradient(to_bottom,black_0%,black_60%,transparent_100%)]
77 [-webkit-mask-image:linear-gradient(to_bottom,black_0%,black_60%,transparent_100%)]
78 [mask-repeat:no-repeat] [-webkit-mask-repeat:no-repeat]
79 [mask-size:100%_100%] [-webkit-mask-size:100%_100%]">
80 <div class="absolute inset-0 bg-slate-200 dark:bg-slate-400
81 [mask-image:url('https://assets.tangled.network/Rope.svg')]
82 [-webkit-mask-image:url('https://assets.tangled.network/Rope.svg')]
83 [mask-repeat:no-repeat] [-webkit-mask-repeat:no-repeat]
84 [mask-position:10%_center] [-webkit-mask-position:10%_center]
85 [mask-size:contain] [-webkit-mask-size:contain]"></div>
86 </div>
87 <div class="relative z-10 flex flex-col gap-20 md:gap-40">
88 {{ template "timeline/fragments/hero" . }}
89 {{ template "timeline/fragments/preview" . }}
90 </div>
91 </div>
92 {{ template "features1" . }}
93 {{ template "features2" . }}
94 {{ template "features3" . }}
95 {{ template "recentUpdates" . }}
96 {{ if .ShowNewsletter }}{{ template "newsletter" . }}{{ end }}
97 </div>
98{{ end }}
99
100{{ block "topbarLayout" . }}
101 <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;">
102 {{ if .LoggedInUser }}
103 <div id="upgrade-banner"
104 hx-get="/upgradeBanner"
105 hx-trigger="load"
106 hx-swap="innerHTML">
107 </div>
108 {{ end }}
109 {{ template "layouts/fragments/topbar" . }}
110 </header>
111{{ end }}
112
113{{ block "footerLayout" . }}
114 <footer class="z-10">
115 {{ template "layouts/fragments/footerFull" . }}
116 </footer>
117{{ end }}
118
119{{ block "bodyClasses" . }}
120 bg-transparent bg-gradient-to-b from-white to-slate-100 dark:bg-none dark:bg-gray-900
121{{ end }}
122
123{{ block "mainLayout" . }}
124 <div class="flex-grow relative">
125 <div
126 class="absolute opacity-50 dark:opacity-5 inset-x-0 top-0 bottom-[-50px] md:bottom-[-150px] pointer-events-none w-full [mask-image:linear-gradient(to_top,transparent_0px,black_250px)] [-webkit-mask-image:linear-gradient(to_top,transparent_0px,black_250px)] [mask-repeat:no-repeat] [-webkit-mask-repeat:no-repeat] [mask-size:100%_100%] [-webkit-mask-size:100%_100%]">
127 <div class="absolute inset-0 bg-slate-200 dark:bg-slate-400 [mask-image:url('https://assets.tangled.network/yarn_ball_mask.svg')] [-webkit-mask-image:url('https://assets.tangled.network/yarn_ball_mask.svg')] [mask-repeat:no-repeat] [-webkit-mask-repeat:no-repeat] [mask-position:right_bottom] [-webkit-mask-position:right_bottom] [mask-size:auto] [-webkit-mask-size:auto]"></div>
128 </div>
129 <div class="max-w-screen-xl mx-auto flex flex-col gap-4 relative z-10">
130 {{ block "contentLayout" . }}
131 <main>
132 {{ block "content" . }}{{ end }}
133 </main>
134 {{ end }}
135
136 {{ block "contentAfterLayout" . }}
137 <main>
138 {{ block "contentAfter" . }}{{ end }}
139 </main>
140 {{ end }}
141 </div>
142 </div>
143{{ end }}
144
145{{ define "features1" }}
146 {{ $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" }}
147 {{ $spanStyle := "z-10 items-center justify-between gap-2 w-full" }}
148 {{ $connectorStyle := "w-0.5 h-6 bg-gray-300 dark:bg-gray-600 opacity-0 mx-auto" }}
149 {{ $contentStyle := "hidden bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 grid-cols-1 animate-fadein" }}
150 {{ $progressOverlayStyle := "absolute inset-0 bg-gray-600/10 dark:bg-gray-100/10 w-0 transition-none" }}
151
152 <style>
153 @media (max-width: 768px) {
154 .features-grid:has(#feature-prs:checked) {
155 grid-template-columns: 1fr auto auto;
156 }
157
158 .features-grid:has(#feature-knots:checked) {
159 grid-template-columns: auto 1fr auto;
160 }
161
162 .features-grid:has(#feature-spindles:checked) {
163 grid-template-columns: auto auto 1fr;
164 }
165
166 #feature-prs:checked ~ label[for="feature-prs"] .label-text,
167 #feature-knots:checked ~ label[for="feature-knots"] .label-text,
168 #feature-spindles:checked ~ label[for="feature-spindles"] .label-text {
169 display: inline-flex !important;
170 }
171 #feature-prs:checked ~ label[for="feature-prs"] .icon-only,
172 #feature-knots:checked ~ label[for="feature-knots"] .icon-only,
173 #feature-spindles:checked ~ label[for="feature-spindles"] .icon-only {
174 display: none !important;
175 }
176 }
177 </style>
178
179 <div class="features-grid w-full grid grid-cols-3 gap-x-6 px-2">
180 <input type="radio" id="feature-prs" name="feature" class="peer/prs hidden" checked />
181 <input type="radio" id="feature-knots" name="feature" class="peer/knots hidden" />
182 <input type="radio" id="feature-spindles" name="feature" class="peer/spindles hidden" />
183
184 <label for="feature-prs" class="{{ $labelStyle }} peer-checked/prs:opacity-100 peer-checked/prs:shadow-sm">
185 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">A better way to review {{ i "git-pull-request" "size-5" }}</span>
186 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "git-pull-request" "size-5" }}</span>
187 <div class="{{ $progressOverlayStyle }}" data-progress="prs"></div>
188 </label>
189
190 <label for="feature-knots" class="{{ $labelStyle }} peer-checked/knots:opacity-100 peer-checked/knots:shadow-sm">
191 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">Completely self-hostable {{ i "hard-drive" "size-5" }}</span>
192 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "hard-drive" "size-5" }}</span>
193 <div class="{{ $progressOverlayStyle }}" data-progress="knots"></div>
194 </label>
195
196 <label for="feature-spindles" class="{{ $labelStyle }} peer-checked/spindles:opacity-100 peer-checked/spindles:shadow-sm">
197 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">Quick and easy CI {{ i "layers-2" "size-5" }}</span>
198 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "layers-2" "size-5" }}</span>
199 <div class="{{ $progressOverlayStyle }}" data-progress="spindles"></div>
200 </label>
201
202 <div class="{{ $connectorStyle }} peer-checked/prs:opacity-100"></div>
203 <div class="{{ $connectorStyle }} peer-checked/knots:opacity-100"></div>
204 <div class="{{ $connectorStyle }} peer-checked/spindles:opacity-100"></div>
205
206 {{ $titleStyle := "text-2xl md:text-6xl my-2 md:mb-4 text-black dark:text-white font-medium" }}
207 {{ $textContentStyle := "p-4 md:p-6 md:text-xl" }}
208 {{ $imgContentStyle := "w-full overflow-hidden place-content-center bg-gradient-to-b from-slate-50 to-slate-100 dark:from-gray-800 dark:to-gray-900 border-t border-gray-200 dark:border-gray-700" }}
209 {{ $linkDesktopStyle := "hover:no-underline items-center gap-2 p-3 text-base btn" }}
210
211 <div class="col-span-3 {{ $contentStyle }} peer-checked/prs:grid">
212 <div class="{{ $textContentStyle }} flex gap-4 md:gap-8">
213 <section class="space-y-4">
214 <h1 class="{{ $titleStyle }}">Pull requests, reimagined</h1>
215 <p class="text-gray-600 dark:text-gray-400">
216 Break down large features into small, reviewable chunks. Stack pull
217 requests on top of each other and ship faster with round-based code
218 reviews. Tangled natively supports stacking using Jujutsu.
219 </p>
220 <a href="https://blog.tangled.org/stacking" class="hover:no-underline inline-flex items-center gap-2 text-sm md:text-base text-black dark:text-white font-medium">
221 Learn more
222 {{ i "arrow-right" "size-4" }}
223 </a>
224 </section>
225 <div class="w-fit hidden md:flex items-center">
226 <div class="size-32 rounded-full flex items-center justify-center bg-yellow-100 dark:bg-yellow-900 -mr-4" >
227 {{ i "wand-sparkles" "size-16 text-yellow-500 dark:text-yellow-500" }}
228 </div>
229 <div class="size-40 rounded-full flex items-center justify-center bg-green-100 dark:bg-green-900 z-10">
230 {{ i "git-pull-request-arrow" "size-24 text-green-500 dark:text-green-500 rotate-12" }}
231 </div>
232 </div>
233 </div>
234 <div class="{{ $imgContentStyle }} relative overflow-hidden flex items-center justify-center">
235 <div class="w-[120%] md:w-full flex">
236 <picture class="w-full">
237 <source srcset="https://assets.tangled.network/home-page-prs-dark.svg" media="(prefers-color-scheme: dark)" />
238 <img src="https://assets.tangled.network/home-page-prs-light.svg" class="w-full block" alt="A list of stacked pull requests with CI checks and PR comments"/>
239 </picture>
240 </div>
241 </div>
242 </div>
243
244 <div class="col-span-3 {{ $contentStyle }} peer-checked/knots:grid">
245 <div class="{{ $textContentStyle }} flex place-content-between gap-4 md:gap-8">
246 <section class="space-y-4">
247 <h1 class="{{ $titleStyle }}">Run it at home</h1>
248 <p class="text-gray-600 dark:text-gray-400">
249 Host your repositories on your own infrastructure with <a href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide" class="no-underline">knots</a>.
250 Run CI on your own machines with <a href="https://docs.tangled.org/spindles.html#self-hosting-guide" class="no-underline">spindles</a>.
251 <br>
252 Don't want to self-host? All are welcome on our hosted instances.
253 </p>
254 <a href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide" class="hover:no-underline inline-flex items-center gap-2 text-sm md:text-base text-black dark:text-white font-medium">
255 Learn more
256 {{ i "arrow-right" "size-4" }}
257 </a>
258 </section>
259 <div class="w-fit hidden md:flex items-center">
260 <div class="size-32 rounded-full flex items-center justify-center bg-blue-100 dark:bg-blue-900 -mr-4" >
261 {{ i "workflow" "size-16 text-blue-500 dark:text-blue-500" }}
262 </div>
263 <div class="size-40 rounded-full flex items-center justify-center bg-yellow-100 dark:bg-yellow-900 z-10">
264 {{ i "server" "size-24 text-yellow-500 dark:text-yellow-500" }}
265 </div>
266 </div>
267 </div>
268 <div class="{{ $imgContentStyle }} relative overflow-hidden flex items-center justify-center">
269 <div class="w-[120%] md:w-full flex">
270 <picture class="w-full">
271 <source srcset="https://assets.tangled.network/home-page-self-host-dark.svg" media="(prefers-color-scheme: dark)" />
272 <img src="https://assets.tangled.network/home-page-self-host-light.svg" class="w-full block" alt="A world map with knots hosting code on servers in different locations across the world" />
273 </picture>
274 </div>
275 </div>
276 </div>
277
278 <div class="col-span-3 {{ $contentStyle }} peer-checked/spindles:grid">
279 <div class="{{ $textContentStyle }} flex place-content-between gap-4 md:gap-8">
280 <section class="space-y-4">
281 <h1 class="{{ $titleStyle }}">Nix-powered CI</h1>
282 <p class="text-gray-600 dark:text-gray-400">
283 Pick and choose dependencies for your CI pipelines from <a
284 href="https://docs.tangled.org/spindles.html#dependencies"
285 class="no-underline"><code>nixpkgs</code></a>, one of the biggest
286 package repositories.
287 <br>
288 Support for Docker and MicroVM based runners coming soon!
289 </p>
290 <a href="https://docs.tangled.org/spindles.html#pipelines" class="hover:no-underline inline-flex items-center gap-2 text-sm md:text-base text-black dark:text-white font-medium">
291 Learn more
292 {{ i "arrow-right" "size-4" }}
293 </a>
294 </section>
295 <div class="w-fit hidden md:flex items-center">
296 <div class="size-32 rounded-full flex items-center justify-center bg-green-100 dark:bg-green-900 -mr-4" >
297 {{ i "package" "size-16 text-green-500 dark:text-green-500" }}
298 </div>
299 <div class="size-40 rounded-full flex items-center justify-center bg-indigo-100 dark:bg-indigo-900 z-10">
300 {{ i "snowflake" "size-24 text-indigo-500 dark:text-indigo-500 rotate-12" }}
301 </div>
302 </div>
303 </div>
304 <div class="{{ $imgContentStyle }} flex items-end justify-center pb-0 overflow-hidden">
305 <div class="w-[120%] md:w-full flex">
306 <picture class="w-full">
307 <source srcset="https://assets.tangled.network/home-page-ci-dark.svg" media="(prefers-color-scheme: dark)" />
308 <img src="https://assets.tangled.network/home-page-ci-light.svg" class="w-full block" alt="The Tangled UI showing status checks, a Nix-based CI workflow YAML file, and logs from a CI job"/>
309 </picture>
310 </div>
311 </div>
312 </div>
313 </div>
314
315 <script>
316 document.addEventListener('DOMContentLoaded', function() {
317 const featureIds = ['feature-prs', 'feature-knots', 'feature-spindles'];
318 const progressNames = ['prs', 'knots', 'spindles'];
319 let currentIndex = 0;
320 let timerInterval = null;
321
322 function stopTimer() {
323 if (timerInterval) {
324 clearInterval(timerInterval);
325 timerInterval = null;
326 }
327 document.querySelectorAll('[data-progress]').forEach(el => {
328 el.classList.remove('animate-progress');
329 });
330 }
331
332 const firstProgress = document.querySelector('[data-progress="prs"]');
333 if (firstProgress) {
334 firstProgress.classList.add('animate-progress');
335 }
336
337 document.querySelectorAll('label[for^="feature-"]').forEach(label => {
338 label.addEventListener('click', stopTimer);
339 });
340
341 timerInterval = setInterval(function() {
342 document.querySelectorAll('[data-progress]').forEach(el => {
343 el.classList.remove('animate-progress');
344 void el.offsetWidth; // force reflow
345 });
346
347 currentIndex = (currentIndex + 1) % featureIds.length;
348 document.getElementById(featureIds[currentIndex]).checked = true;
349
350 const activeProgress = document.querySelector('[data-progress="' + progressNames[currentIndex] + '"]');
351 if (activeProgress) {
352 activeProgress.classList.add('animate-progress');
353 }
354 }, 10000);
355 });
356 </script>
357{{ end }}
358
359{{ define "features2" }}
360 {{ $cardStyle := "w-full max-w-[548px] bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 p-4 md:p-6 flex flex-col" }}
361 {{ $imgBoxStyle := "w-full rounded overflow-hidden outline outline-1 -outline-offset-1 outline-gray-200 dark:outline-gray-700 bg-gradient-to-b from-slate-50 to-slate-100 dark:from-gray-800 dark:to-gray-900 mb-6" }}
362 {{ $titleStyle := "text-2xl md:text-3xl font-bold mb-2 md:mb-3 text-gray-900 dark:text-gray-100" }}
363 {{ $descriptionStyle := "text-gray-600 dark:text-gray-300 leading-relaxed mb-6 flex-1" }}
364 {{ $linkStyle := "hover:no-underline inline-flex items-center gap-2 text-sm md:text-base text-black dark:text-white font-medium" }}
365
366 <div class="w-full max-w-screen-xl mx-auto px-2">
367 <div class="text-center mb-10 md:mb-16 space-y-3">
368 <h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100">Tools to help you work</h2>
369 <p class="text-lg text-gray-600 dark:text-gray-400">For when the internet shows up in your inbox.</p>
370 </div>
371
372 <div class="flex flex-col md:flex-row items-center md:items-stretch md:justify-center gap-6">
373 <div class="{{ $cardStyle }}">
374 <div class="{{ $imgBoxStyle }}">
375 <picture class="block">
376 <source srcset="https://assets.tangled.network/home-page-vouching-dark.svg" media="(prefers-color-scheme: dark)" />
377 <img src="https://assets.tangled.network/home-page-vouching-light.svg" class="block w-full h-auto" alt="A globe with contributors carrying coloured trust shields, connected in a web" />
378 </picture>
379 </div>
380 <h3 class="{{ $titleStyle }}">A web of trust</h3>
381 <p class="{{ $descriptionStyle }}">
382 Vouch for the people you collaborate with. Trusted contributors carry
383 a green shield right where you interact with them, so you and your
384 circle can tell real work from LLM spam at a glance.
385 </p>
386 <a href="https://blog.tangled.org/vouching/" class="{{ $linkStyle }}">
387 Learn more
388 {{ i "arrow-right" "size-4" }}
389 </a>
390 </div>
391
392 <div class="{{ $cardStyle }}">
393 <div class="{{ $imgBoxStyle }}">
394 <picture class="block">
395 <source srcset="https://assets.tangled.network/home-page-focus-dark.svg" media="(prefers-color-scheme: dark)" />
396 <img src="https://assets.tangled.network/home-page-focus-light.svg" class="block w-full h-auto" alt="A focus-mode inbox stepping through issues, pull requests, and mentions one at a time" />
397 </picture>
398 </div>
399 <h3 class="{{ $titleStyle }}">Focus mode</h3>
400 <p class="{{ $descriptionStyle }}">
401 Focus Mode is a new way to clear notifications: lock in and knock out
402 your list one item at a time, with a task bar that nudges you to stay
403 on track until the queue is empty.
404 </p>
405 <a href="https://blog.tangled.org/newsletter-03/#focus-mode" class="{{ $linkStyle }}">
406 Learn more
407 {{ i "arrow-right" "size-4" }}
408 </a>
409 </div>
410 </div>
411 </div>
412{{ end }}
413
414{{ define "features3" }}
415 {{ $cardStyle := "bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700" }}
416 {{ $cardInnerStyle := "flex flex-row items-center gap-4 md:gap-6 p-4 md:p-6 md:pt-8 relative" }}
417 {{ $contentStyle := "flex-1 flex flex-col" }}
418 {{ $titleStyle := "text-xl md:text-2xl font-bold mb-2 md:mb-3 text-gray-900 dark:text-gray-100" }}
419 {{ $descriptionStyle := "text-gray-600 dark:text-gray-300 text-sm md:text-base leading-relaxed mb-5 md:mb-0" }}
420 {{ $linkMobileStyle := "hover:no-underline inline-flex md:hidden items-center gap-2 text-sm text-black dark:text-white font-medium" }}
421 {{ $iconContainerStyle := "relative shrink-0 w-24 h-24 md:w-48 md:h-48" }}
422 {{ $iconCircleStyle := "rounded-full flex items-center justify-center border border-gray-200 dark:border-gray-700" }}
423 {{ $linkDesktopStyle := "hover:no-underline hidden md:inline-flex absolute -bottom-10 -right-14 items-center gap-2 p-3 text-base btn" }}
424
425 <div class="relative">
426 <div class="hidden md:block absolute -top-32 -bottom-32 inset-x-0 pointer-events-none z-0 opacity-50 dark:opacity-5
427 [mask-image:linear-gradient(to_bottom,transparent_0%,black_15%,black_85%,transparent_100%)]
428 [-webkit-mask-image:linear-gradient(to_bottom,transparent_0%,black_15%,black_85%,transparent_100%)]
429 [mask-repeat:no-repeat] [-webkit-mask-repeat:no-repeat]
430 [mask-size:100%_100%] [-webkit-mask-size:100%_100%]">
431 <div class="absolute inset-0 bg-slate-200 dark:bg-slate-400
432 [mask-image:url('https://assets.tangled.network/rope-2.svg')]
433 [-webkit-mask-image:url('https://assets.tangled.network/rope-2.svg')]
434 [mask-repeat:no-repeat] [-webkit-mask-repeat:no-repeat]
435 [mask-position:-400px_center] [-webkit-mask-position:-400px_center]
436 [mask-size:auto_200%] [-webkit-mask-size:auto_200%]"></div>
437 </div>
438 <div class="relative z-10 w-full flex flex-col gap-6 md:gap-40 max-w-5xl mx-auto px-2">
439 <div class="{{ $cardStyle }} md:mr-32">
440 <div class="{{ $cardInnerStyle }}">
441 {{ $moreLink := "https://docs.tangled.org/quick-start-guide.html#login-or-sign-up" }}
442 <div class="{{ $contentStyle }}">
443 <h2 class="{{ $titleStyle }}">Built on AT Protocol</h2>
444 <p class="{{ $descriptionStyle }}">
445 <a class="underline" href="https://atproto.com/">AT Protocol</a> enables federated code-collaboration. Submit
446 pull-requests or bug-reports to any repository hosted on any
447 server.
448 <br>
449 <br>
450 Bring an existing AT Protocol account (such as your Bluesky
451 account), or signup for one with us.
452 </p>
453 <a href="{{ $moreLink }}" class="{{ $linkMobileStyle }}">
454 Learn more
455 {{ i "arrow-right" "size-4" }}
456 </a>
457 </div>
458 <div class="{{ $iconContainerStyle }}">
459 <div class="{{ $iconCircleStyle }} w-full h-full bg-blue-100 dark:bg-blue-900/50">
460 {{ i "at-sign" "size-12 md:size-24" "text-blue-500 dark:text-blue-500" "rotate-12" }}
461 </div>
462 <div class="{{ $iconCircleStyle }} absolute -top-2 -left-2 w-10 h-10 md:w-16 md:h-16 rounded-full bg-white dark:bg-gray-800 -rotate-12">
463 {{ i "globe" "size-4 md:size-8" "text-blue-500 dark:text-blue-500" }}
464 </div>
465 <a href="{{ $moreLink }}" class="{{ $linkDesktopStyle }}">
466 Learn more
467 {{ i "arrow-right" "size-4" }}
468 </a>
469 </div>
470 </div>
471 </div>
472
473 <div class="{{ $cardStyle }} md:ml-32">
474 <div class="{{ $cardInnerStyle }}">
475 {{ $moreLink := "https://tangled.org/core" }}
476 <div class="{{ $contentStyle }}">
477 <h2 class="{{ $titleStyle }}">Free and open source</h2>
478 <p class="{{ $descriptionStyle }}">
479 All of Tangled is open source and built with the community!
480 Check out the <a class="underline" href="https://tangled.org/core">monorepo</a> and join in on the fun.
481 <br>
482 <br>
483 We welcome contributions however big or small. You can start contributing by picking up a
484 <a class="underline" href="https://tangled.org/tangled.org/core/issues?q=state%3Aopen+label%3Agood-first-issue">good-first-issue</a>.
485 </p>
486 <a href="{{ $moreLink }}" class="{{ $linkMobileStyle }}">
487 View source
488 {{ i "arrow-right" "size-4" }}
489 </a>
490 </div>
491 <div class="{{ $iconContainerStyle }}">
492 <div class="{{ $iconCircleStyle }} w-full h-full bg-green-100 dark:bg-green-900/50">
493 {{ i "package-open" "size-12 md:size-24" "text-green-500 dark:text-green-500" "-rotate-12" }}
494 </div>
495 <div class="absolute -top-2 -left-2 w-10 h-10 md:w-16 md:h-16 rounded-full bg-white dark:bg-gray-800 flex items-center justify-center border border-gray-200 dark:border-gray-700 rotate-12">
496 {{ i "heart" "size-4 md:size-8" "text-green-500 dark:text-green-500" }}
497 </div>
498 <a href="{{ $moreLink }}" class="{{ $linkDesktopStyle }}">
499 View source
500 {{ i "arrow-right" "size-4" }}
501 </a>
502 </div>
503 </div>
504 </div>
505
506 <div class="{{ $cardStyle }} md:mr-32">
507 <div class="{{ $cardInnerStyle }}">
508 {{ $moreLink := "/timeline" }}
509 <div class="{{ $contentStyle }}">
510 <h2 class="{{ $titleStyle }}">Social coding is back</h2>
511 <p class="{{ $descriptionStyle }}">
512 Discover trending projects, follow your friends and star your favourite repositories. Coding is better together!
513 </p>
514 <a href="{{ $moreLink }}" class="{{ $linkMobileStyle }}">
515 Explore timeline
516 {{ i "arrow-right" "size-4" }}
517 </a>
518 </div>
519 <div class="{{ $iconContainerStyle }}">
520 <div class="{{ $iconCircleStyle }} w-full h-full bg-amber-100 dark:bg-amber-900/50">
521 {{ i "message-circle-heart" "size-12 md:size-24" "text-amber-500 dark:text-amber-500" "rotate-12" }}
522 </div>
523 <div class="absolute -top-2 -left-2 w-10 h-10 md:w-16 md:h-16 rounded-full bg-white dark:bg-gray-800 flex items-center justify-center border border-gray-200 dark:border-gray-700 -rotate-12">
524 {{ i "users" "size-4 md:size-8" "text-amber-500 dark:text-amber-500" }}
525 </div>
526 <a href="{{ $moreLink }}" class="{{ $linkDesktopStyle }}">
527 Explore timeline
528 {{ i "arrow-right" "size-4" }}
529 </a>
530 </div>
531 </div>
532 </div>
533 </div>
534 </div>
535{{ end }}
536
537{{ define "changelog" }}
538 <div class="w-full px-2 py-16 md:py-24">
539 <h2 class="text-3xl md:text-4xl font-bold mb-8 text-gray-900 dark:text-gray-100">Changelog</h2>
540
541 <div class="grid grid-cols-1 gap-6 mb-6">
542 {{ range $index := list 0 1 2 3 }}
543 <div class="bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 p-6">
544 <div class="flex items-center gap-2 mb-3">
545 <span class="text-xs font-medium text-gray-500 dark:text-gray-400">v1.{{ sub 3 $index }}.0</span>
546 <span class="text-xs text-gray-400 dark:text-gray-500">•</span>
547 <span class="text-xs text-gray-500 dark:text-gray-400">Feb {{ sub 16 $index }}, 2026</span>
548 </div>
549 <h3 class="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100">Feature Update {{ sub 4 $index }}</h3>
550 <p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
551 Improvements to the platform including bug fixes, performance enhancements, and new features.
552 </p>
553 </div>
554 {{ end }}
555 </div>
556
557 <div class="flex justify-end">
558 <a href="/changelog" class="hover:no-underline inline-flex items-center gap-2 px-4 py-2 text-base btn">
559 View full changelog
560 {{ i "arrow-right" "size-4" }}
561 </a>
562 </div>
563 </div>
564{{ end }}
565
566{{ define "recentUpdates" }}
567 <div class="w-full px-2 py-16 md:py-24">
568 <h2 class="px-4 text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2">Recent updates</h2>
569 <p class="px-4 mb-8 text-gray-500 dark:text-gray-400">
570 Follow <a href="https://bsky.app/profile/tangled.org">@tangled.org</a> on Bluesky for more!
571 </p>
572 <div class="columns-1 md:columns-2 lg:columns-3 gap-6">
573 {{ range $index, $post := .BlueskyPosts }}
574 <div class="{{ if ge $index 3 }}hidden md:block{{ end }}">
575 {{ template "post" $post }}
576 </div>
577 {{ end }}
578 </div>
579 </div>
580{{ end }}
581
582{{ define "post" }}
583 {{ $author := or .AuthorDid "tangled.org" }}
584 <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">
585 <div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
586 <a href="https://bsky.app/profile/{{ $author }}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 no-underline hover:no-underline">
587 {{ template "user/fragments/picHandle" $author }}
588 </a>
589 <span>{{ template "repo/fragments/shortTimeAgo" .CreatedAt }}</span>
590 </div>
591 <p class="text-gray-900 dark:text-gray-100 text-base leading-relaxed whitespace-pre-wrap">{{ .Text }}</p>
592
593 {{ if .Embed }}
594 {{ if .Embed.Images }}
595 {{ if eq (len .Embed.Images) 1 }}
596 {{ with index .Embed.Images 0 }}
597 <img src="{{ .Fullsize }}" alt="{{ .Alt }}"
598 class="rounded w-full h-auto object-cover border border-gray-200 dark:border-gray-700"
599 loading="lazy" />
600 {{ end }}
601 {{ else }}
602 {{ $total := len .Embed.Images }}
603 <div class="flex overflow-x-auto gap-1.5 rounded"
604 style="scrollbar-width: none; -ms-overflow-style: none;"
605 data-bsky-carousel>
606 {{ range $i, $img := .Embed.Images }}
607 <div class="relative flex-shrink-0 h-full">
608 <img src="{{ $img.Fullsize }}" alt="{{ $img.Alt }}"
609 class="h-full w-auto rounded object-cover border border-gray-200 dark:border-gray-700"
610 loading="lazy"
611 {{ if $img.AspectRatio }}data-aspect="{{ $img.AspectRatio.Width }}:{{ $img.AspectRatio.Height }}"{{ end }} />
612 <span class="absolute top-2 right-2 bg-black/60 text-white text-xs font-medium px-1.5 py-0.5 rounded pointer-events-none select-none tabular-nums">{{ add $i 1 }}/{{ $total }}</span>
613 </div>
614 {{ end }}
615 </div>
616 {{ end }}
617 {{ else if .Embed.External }}
618 <a href="{{ .Embed.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">
619 {{ if .Embed.External.Thumb }}
620 <img src="{{ .Embed.External.Thumb }}" alt="" class="w-full h-48 object-cover" loading="lazy" />
621 {{ end }}
622 <div class="p-3">
623 <div class="font-medium text-gray-900 dark:text-gray-100 text-sm mb-1">{{ .Embed.External.Title }}</div>
624 <div class="text-xs text-gray-600 dark:text-gray-400 line-clamp-2">{{ .Embed.External.Description }}</div>
625 <div class="text-xs text-gray-500 dark:text-gray-500 mt-1">{{ .Embed.External.Uri }}</div>
626 </div>
627 </a>
628 {{ else if .Embed.Video }}
629 <video
630 class="rounded w-full h-auto max-h-[32rem] bg-black border border-gray-200 dark:border-gray-700"
631 controls
632 preload="none"
633 playsinline
634 {{ with .Embed.Video.Thumbnail }}poster="{{ . }}"{{ end }}
635 {{ with .Embed.Video.Alt }}aria-label="{{ . }}"{{ end }}
636 data-hls="{{ .Embed.Video.Playlist }}"
637 ></video>
638 {{ end }}
639 {{ end }}
640
641 <a href="https://bsky.app/profile/{{ $author }}/post/{{ .Rkey }}"
642 target="_blank"
643 rel="noopener noreferrer"
644 class="flex items-center justify-between gap-4 text-sm text-gray-500 dark:text-gray-400 pt-2 no-underline hover:no-underline">
645 <div class="flex items-center gap-4">
646 <div class="flex items-center gap-1">
647 {{ i "heart" "size-4" }}
648 <span>{{ .LikeCount }}</span>
649 </div>
650 <div class="flex items-center gap-1">
651 {{ i "repeat-2" "size-4" }}
652 <span>{{ .RepostCount }}</span>
653 </div>
654 <div class="flex items-center gap-1">
655 {{ i "message-square" "size-4 -scale-x-1" }}
656 <span>{{ add64 .ReplyCount .QuoteCount }}</span>
657 </div>
658 </div>
659 {{ i "arrow-up-right" "size-4" }}
660 </a>
661 </div>
662{{ end }}
663
664{{ define "newsletter" }}
665 <div class="w-full px-2 py-16 md:py-24">
666 <div class="max-w-2xl mx-auto text-center space-y-6">
667 <h2 class="text-3xl md:text-6xl font-bold text-gray-900 dark:text-gray-100">Stay in the loop</h2>
668 <p class="text-xl text-gray-600 dark:text-gray-300">
669 We've got a newsletter! Punch in your email to stay updated on what we're shipping at Tangled.
670 </p>
671 {{ template "timeline/fragments/newsletterForm" (dict "Id" "home" "Variant" "hero") }}
672 </div>
673 </div>
674{{ end }}