Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Svelte
at icy/tmrrpn
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462<script lang="ts"> import { resolve } from "$app/paths"; import Logo from "../ui/Logo.svelte";
interface Props { variant?: "full" | "minimal"; }
type FooterIcon = | "terms" | "privacy" | "blog" | "docs" | "source" | "brand" | "discord" | "irc" | "bluesky" | "twitter" | "email" | "security"; type FooterLayout = "desktop" | "mobile" | "minimal";
interface FooterLinkAttributes { target?: "_blank"; }
interface FooterLink { label: string; href: string; icon: FooterIcon; internal?: true; minimalLabel?: string; minimalOrder?: number; attributes?: FooterLinkAttributes; desktopAttributes?: FooterLinkAttributes; }
interface FooterSection { title: string; links: FooterLink[]; }
let { variant = "full" }: Props = $props();
const appPath = (path: string) => resolve(path as "/"); const headerStyle = "text-foreground-default font-bold typography-paragraph-small tracking-wide mb-1"; const mobileHeaderStyle = "text-foreground-default font-bold typography-paragraph-small tracking-wide mb-1"; const linkStyle = "inline-flex items-center gap-1 text-foreground-subtle hover:text-foreground-default hover:underline"; const minimalLinkStyle = "text-foreground-subtle hover:text-foreground-default hover:underline"; const externalLinkAttributes = { target: "_blank" } as const;
const footerSections: FooterSection[] = [ { title: "Legal", links: [ { label: "Terms of service", minimalLabel: "Terms", href: "/terms", internal: true, icon: "terms", minimalOrder: 8 }, { label: "Privacy policy", minimalLabel: "Privacy", href: "/privacy", internal: true, icon: "privacy", minimalOrder: 9 } ] }, { title: "Resources", links: [ { label: "Blog", href: "https://blog.tangled.org", icon: "blog", minimalOrder: 1, desktopAttributes: externalLinkAttributes }, { label: "Docs", href: "https://docs.tangled.org", icon: "docs", minimalOrder: 2 }, { label: "Source", href: "https://tangled.org/@tangled.org/core", icon: "source", minimalOrder: 3 }, { label: "Brand", href: "https://tangled.org/brand", icon: "brand", minimalOrder: 4 } ] }, { title: "Social", links: [ { label: "Discord", href: "https://chat.tangled.org", icon: "discord", minimalOrder: 5, attributes: externalLinkAttributes }, { label: "IRC", href: "https://web.libera.chat/#tangled", icon: "irc", attributes: externalLinkAttributes }, { label: "Bluesky", href: "https://bsky.app/profile/tangled.org", icon: "bluesky", minimalOrder: 6, attributes: externalLinkAttributes }, { label: "Twitter (X)", href: "https://x.com/tangled_org", icon: "twitter", minimalOrder: 7, attributes: externalLinkAttributes } ] }, { title: "Contact", links: [ { label: "team@tangled.org", href: "mailto:team@tangled.org", icon: "email" }, { label: "security@tangled.org", href: "mailto:security@tangled.org", icon: "security" } ] } ];
const minimalFooterLinks = footerSections .flatMap((section) => section.links) .filter((link) => typeof link.minimalOrder === "number") .sort((a, b) => (a.minimalOrder ?? 0) - (b.minimalOrder ?? 0));
const linkAttributes = (link: FooterLink, layout: FooterLayout) => layout === "desktop" ? (link.desktopAttributes ?? link.attributes) : link.attributes; const linkTarget = (link: FooterLink, layout: FooterLayout) => linkAttributes(link, layout)?.target;</script>
{#snippet footerIcon(icon: FooterIcon)} {#if icon === "terms"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /> <path d="M14 2v4a2 2 0 0 0 2 2h4" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /> </svg> {:else if icon === "privacy"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M20 13c0 5-3.5 7.5-7.66 9.7a1 1 0 0 1-.68 0C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .76-.97l8-2a1 1 0 0 1 .48 0l8 2c.47.12.76.54.76.97Z" /> </svg> {:else if icon === "blog"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" /> <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" /> </svg> {:else if icon === "docs"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M4 19.5V15a2 2 0 0 1 2-2h2M20 19.5V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2z" /> </svg> {:else if icon === "source"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <polyline points="16 18 22 12 16 6" /> <polyline points="8 6 2 12 8 18" /> </svg> {:else if icon === "brand"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /> <path d="M7.5 10.5c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5z" /> <path d="M11.5 7.5c.828 0 1.5-.672 1.5-1.5S12.328 4.5 11.5 4.5 10 5.172 10 6s.672 1.5 1.5 1.5z" /> <path d="M16.5 9.5c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5z" /> <path d="M6 14c0-2 2-3 4-3 2.5 0 4.5 1.5 4.5 3.5S12.5 18 10 18c-2.5 0-4-2-4-4z" /> </svg> {:else if icon === "discord"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /> </svg> {:else if icon === "irc"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <line x1="4" y1="9" x2="20" y2="9" /> <line x1="4" y1="15" x2="20" y2="15" /> <line x1="10" y1="3" x2="8" y2="21" /> <line x1="16" y1="3" x2="14" y2="21" /> </svg> {:else if icon === "bluesky"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke-dasharray="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M12 21a9 9 0 0 0 9-9 9 9 0 0 0-9-9 9 9 0 0 0-9 9 9 9 0 0 0 9 9Z" /> <path d="M12 12c2-3 5-3 5 0s-3 3-5 0Zm0 0c-2-3-5-3-5 0s3 3 5 0Z" /> </svg> {:else if icon === "twitter"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="currentColor"> <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /> </svg> {:else if icon === "email"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <rect width="20" height="16" x="2" y="4" rx="2" /> <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /> </svg> {:else if icon === "security"} <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M20 13c0 5-3.5 7.5-7.66 9.7a1 1 0 0 1-.68 0C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .76-.97l8-2a1 1 0 0 1 .48 0l8 2c.47.12.76.54.76.97Z" /> <path d="m9 12 2 2 4-4" /> </svg> {/if}{/snippet}
{#if variant === "minimal"} <footer class="w-full border-t border-border-default bg-background-default px-6 py-4 pb-[calc(env(safe-area-inset-bottom)+1rem)]" > <div class="mx-auto flex max-w-5xl flex-wrap items-center justify-center gap-x-4 gap-y-2 typography-paragraph-small text-foreground-subtle" > <div class="order-last flex w-full items-center justify-center gap-x-2 sm:order-first sm:w-auto" > <a href={resolve("/")} class="flex items-center no-underline hover:no-underline"> <img src="/logos/dolly.svg" alt="tangled" class="size-5 opacity-70" /> </a> <span>© 2026 Tangled Labs Oy.</span> </div> {#each minimalFooterLinks as link (link.href)} {#if link.internal} <a href={appPath(link.href)} class={minimalLinkStyle} >{link.minimalLabel ?? link.label}</a > {:else} <a href={link.href} class={minimalLinkStyle} target={linkTarget(link, "minimal")} rel="external noopener noreferrer">{link.minimalLabel ?? link.label}</a > {/if} {/each} </div> </footer>{:else} <footer class="z-10 pb-[env(safe-area-inset-bottom)] select-none"> <div class="w-full p-6"> <div class="mx-auto"> <div class="flex flex-col gap-8 text-foreground-subtle"> <div class="hidden lg:grid lg:grid-cols-[1fr_minmax(0,1024px)_1fr] lg:items-start lg:gap-8" > <div> <a href={resolve("/")} class="flex gap-2 no-underline hover:no-underline" > <Logo wordmark alpha class="h-6.5" /> </a> </div>
<div class="grid grid-cols-4 gap-2"> {#each footerSections as section (section.title)} <div class="flex flex-col gap-1"> <div class={headerStyle}>{section.title}</div> {#each section.links as link (link.href)} {#if link.internal} <a href={appPath(link.href)} class={linkStyle}> {@render footerIcon(link.icon)} <span>{link.label}</span> </a> {:else} <a href={link.href} class={linkStyle} target={linkTarget(link, "desktop")} rel="external noopener noreferrer" > {@render footerIcon(link.icon)} <span>{link.label}</span> </a> {/if} {/each} </div> {/each} </div>
<div class="text-right"> <div class="typography-paragraph-small"> © 2026 Tangled Labs Oy. All rights reserved. </div> </div> </div>
<div class="flex flex-col gap-8 lg:hidden"> <div class="mb-4 md:mb-0"> <a href={resolve("/")} class="flex gap-2 no-underline hover:no-underline" > <Logo wordmark alpha class="h-6.5" /> </a> </div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-4 md:gap-2"> {#each footerSections as section (section.title)} <div class="flex flex-col gap-1"> <div class={mobileHeaderStyle}>{section.title}</div> {#each section.links as link (link.href)} {#if link.internal} <a href={appPath(link.href)} class={linkStyle} >{link.label}</a > {:else} <a href={link.href} class={linkStyle} target={linkTarget(link, "mobile")} rel="external noopener noreferrer">{link.label}</a > {/if} {/each} </div> {/each} </div>
<div class="text-center"> <div class="typography-paragraph-small"> © 2026 Tangled Labs Oy. All rights reserved. </div> </div> </div> </div> </div> </div> </footer>{/if}