Monorepo for Tangled tangled.org
1

Configure Feed

Select the types of activity you want to include in your feed.

web: formatting, stories & de-dups

- add prettier-plugin-tailwindcss to auto-sort tailwind classes
- de-duplicate components
- add missing stories
- fix a few lint nits & errors

Signed-off-by: eti <eti@eti.tf>

author
eti
committer
dawn
date (Jul 27, 2026, 3:40 PM +0300) commit 47315e33 parent 03667bff
+424 -263
+1 -1
web/package.json
··· 39 39 "globals": "^17.6.0", 40 40 "prettier": "^3.8.3", 41 41 "prettier-plugin-svelte": "^4.1.0", 42 - "prettier-plugin-tailwindcss": "^0.8.0", 42 + "prettier-plugin-tailwindcss": "^0.8.1", 43 43 "svelte": "^5.56.1", 44 44 "svelte-check": "^4.6.0", 45 45 "tailwindcss": "^4.3.2",
+5 -5
web/pnpm-lock.yaml
··· 106 106 specifier: ^4.1.0 107 107 version: 4.1.1(prettier@3.9.4)(svelte@5.56.4(@typescript-eslint/types@8.63.0)) 108 108 prettier-plugin-tailwindcss: 109 - specifier: ^0.8.0 110 - version: 0.8.0(prettier-plugin-svelte@4.1.1(prettier@3.9.4)(svelte@5.56.4(@typescript-eslint/types@8.63.0)))(prettier@3.9.4) 109 + specifier: ^0.8.1 110 + version: 0.8.1(prettier-plugin-svelte@4.1.1(prettier@3.9.4)(svelte@5.56.4(@typescript-eslint/types@8.63.0)))(prettier@3.9.4) 111 111 storybook: 112 112 specifier: ^10.5.0 113 113 version: 10.5.0(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7) ··· 2283 2283 prettier: ^3.0.0 2284 2284 svelte: ^5.0.0 2285 2285 2286 - prettier-plugin-tailwindcss@0.8.0: 2287 - resolution: {integrity: sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g==} 2286 + prettier-plugin-tailwindcss@0.8.1: 2287 + resolution: {integrity: sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==} 2288 2288 engines: {node: '>=20.19'} 2289 2289 peerDependencies: 2290 2290 '@ianvs/prettier-plugin-sort-imports': '*' ··· 4752 4752 prettier: 3.9.4 4753 4753 svelte: 5.56.4(@typescript-eslint/types@8.63.0) 4754 4754 4755 - prettier-plugin-tailwindcss@0.8.0(prettier-plugin-svelte@4.1.1(prettier@3.9.4)(svelte@5.56.4(@typescript-eslint/types@8.63.0)))(prettier@3.9.4): 4755 + prettier-plugin-tailwindcss@0.8.1(prettier-plugin-svelte@4.1.1(prettier@3.9.4)(svelte@5.56.4(@typescript-eslint/types@8.63.0)))(prettier@3.9.4): 4756 4756 dependencies: 4757 4757 prettier: 3.9.4 4758 4758 optionalDependencies:
+2 -1
web/prettier.config.js
··· 6 6 printWidth: 100, 7 7 plugins: ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 8 8 overrides: [{ files: "*.svelte", options: { parser: "svelte" } }], 9 - tailwindStylesheet: "./src/app.css" 9 + tailwindStylesheet: "./src/app.css", 10 + tailwindFunctions: ["tv"] 10 11 }; 11 12 12 13 export default config;
+2 -2
web/src/lib/components/home/MarketingHome.svelte
··· 45 45 </form> 46 46 {/snippet} 47 47 48 - <section class="overflow-hidden px-6 pb-24 pt-28 md:pb-32 md:pt-36"> 48 + <section class="overflow-hidden px-6 pt-28 pb-24 md:pt-36 md:pb-32"> 49 49 <div class="mx-auto grid max-w-screen-xl grid-cols-1 items-center gap-14 md:grid-cols-2"> 50 50 <h1 51 - class="text-6xl font-bold leading-[0.98] tracking-[-0.05em] text-foreground-default md:text-7xl lg:text-8xl" 51 + class="text-6xl leading-[0.98] font-bold tracking-[-0.05em] text-foreground-default md:text-7xl lg:text-8xl" 52 52 > 53 53 tightly-knit<br />social coding. 54 54 </h1>
+1 -1
web/src/lib/components/profile/EmptyState.svelte web/src/lib/components/ui/EmptyState.svelte
··· 10 10 </script> 11 11 12 12 <div 13 - class="flex items-center justify-center border border-border-default p-12 text-base text-foreground-subtle italic" 13 + class="flex items-center justify-center rounded-sm border border-border-default p-12 text-base text-foreground-subtle italic" 14 14 > 15 15 <span class="flex items-center gap-2">{message}{@render children?.()}</span> 16 16 </div>
+3 -3
web/src/lib/components/profile/FollowCard.svelte
··· 25 25 <div class="flex min-h-0 flex-1 flex-col justify-around"> 26 26 <a 27 27 href={resolve(`/${person.handle}` as "/")} 28 - class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap font-bold text-foreground-default hover:underline" 28 + class="max-w-full overflow-hidden font-bold text-ellipsis whitespace-nowrap text-foreground-default hover:underline" 29 29 > 30 30 {person.handle} 31 31 </a> 32 32 {#if person.description} 33 - <p class="break-words pb-2 text-sm text-foreground-muted md:pb-2">{person.description}</p> 33 + <p class="pb-2 text-sm break-words text-foreground-muted md:pb-2">{person.description}</p> 34 34 {/if} 35 35 <div 36 - class="my-2 flex max-w-full items-center gap-2 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-foreground-subtle" 36 + class="my-2 flex max-w-full items-center gap-2 overflow-hidden text-sm text-ellipsis whitespace-nowrap text-foreground-subtle" 37 37 > 38 38 <Users class="size-4 shrink-0" /> 39 39 <span
+20 -46
web/src/lib/components/profile/ProfileTabs.svelte
··· 1 1 <script lang="ts"> 2 - import { resolve } from "$app/paths"; 3 2 import SquareChartGantt from "$icon/square-chart-gantt"; 4 3 import BookMarked from "$icon/book-marked"; 5 4 import Star from "$icon/star"; 6 5 import LineSquiggle from "$icon/line-squiggle"; 7 6 import Shield from "$icon/shield"; 8 - import type { Component } from "svelte"; 9 - import type { SvelteHTMLElements } from "svelte/elements"; 7 + import Tabs, { type TabDef } from "$lib/components/ui/Tabs.svelte"; 10 8 import type { ProfileCounts } from "./types"; 11 9 12 10 interface Props { ··· 17 15 18 16 let { handle, active, counts }: Props = $props(); 19 17 20 - interface TabDef { 21 - id: string; 22 - label: string; 23 - icon: Component<SvelteHTMLElements["svg"]>; 24 - count?: number; 25 - } 18 + const hrefFor = (id: string) => (id === "overview" ? `/${handle}` : `/${handle}?tab=${id}`); 26 19 27 20 const tabs = $derived<TabDef[]>([ 28 - { id: "overview", label: "Overview", icon: SquareChartGantt }, 29 - { id: "repos", label: "Repositories", icon: BookMarked, count: counts.repos }, 30 - { id: "starred", label: "Starred", icon: Star, count: counts.stars }, 31 - { id: "strings", label: "Strings", icon: LineSquiggle, count: counts.strings }, 32 - { id: "vouches", label: "Vouches", icon: Shield } 21 + { id: "overview", label: "Overview", icon: SquareChartGantt, href: hrefFor("overview") }, 22 + { 23 + id: "repos", 24 + label: "Repositories", 25 + icon: BookMarked, 26 + count: counts.repos, 27 + href: hrefFor("repos") 28 + }, 29 + { id: "starred", label: "Starred", icon: Star, count: counts.stars, href: hrefFor("starred") }, 30 + { 31 + id: "strings", 32 + label: "Strings", 33 + icon: LineSquiggle, 34 + count: counts.strings, 35 + href: hrefFor("strings") 36 + }, 37 + { id: "vouches", label: "Vouches", icon: Shield, href: hrefFor("vouches") } 33 38 ]); 34 - 35 - const hrefFor = (id: string) => 36 - resolve((id === "overview" ? `/${handle}` : `/${handle}?tab=${id}`) as "/"); 37 39 </script> 38 40 39 - <nav class="w-full overflow-x-auto overflow-y-hidden pl-4" aria-label="profile sections"> 40 - <div class="flex"> 41 - {#each tabs as tab (tab.id)} 42 - {@const isActive = active === tab.id} 43 - {@const Glyph = tab.icon} 44 - <a 45 - href={hrefFor(tab.id)} 46 - class="group relative -mr-px no-underline hover:no-underline" 47 - aria-current={isActive ? "page" : undefined} 48 - > 49 - <div 50 - class={`relative mr-1 min-w-[80px] rounded-t px-4 py-1 text-center whitespace-nowrap text-foreground-default ${ 51 - isActive 52 - ? "-mb-px bg-background-default font-medium" 53 - : "group-hover:bg-background-inset/25" 54 - }`} 55 - > 56 - <span class="flex items-center justify-center"> 57 - <Glyph class="mr-2 h-4 w-4" aria-hidden="true" /> 58 - {tab.label} 59 - {#if tab.count} 60 - <span class="ml-1 rounded bg-background-inset px-1 text-sm">{tab.count}</span> 61 - {/if} 62 - </span> 63 - </div> 64 - </a> 65 - {/each} 66 - </div> 67 - </nav> 41 + <Tabs {tabs} {active} label="profile sections" />
+12 -13
web/src/lib/components/profile/tabs/OverviewTab.svelte
··· 1 1 <script lang="ts"> 2 2 import RepoCard from "$lib/components/repo/RepoCard.svelte"; 3 - import EmptyState from "../EmptyState.svelte"; 3 + import Section from "$lib/components/ui/Section.svelte"; 4 4 import ActivityStub from "../ActivityStub.svelte"; 5 5 import type { RepoCardData } from "../types"; 6 6 ··· 12 12 </script> 13 13 14 14 <div class="grid gap-6 md:grid-cols-8"> 15 - <section class="order-1 md:col-span-4"> 16 - <h2 class="px-2 pb-4 text-base font-medium">Pinned repositories</h2> 17 - {#if pinned.length === 0} 18 - <EmptyState message="No pinned repositories." /> 19 - {:else} 20 - <div class="flex flex-col gap-4"> 21 - {#each pinned as repo (repo.rkey)} 22 - <RepoCard {repo} starButton={false} showOwner={false} /> 23 - {/each} 24 - </div> 25 - {/if} 26 - </section> 15 + <div class="order-1 md:col-span-4"> 16 + <Section 17 + title="Pinned repositories" 18 + empty={pinned.length === 0} 19 + emptyMessage="No pinned repositories." 20 + > 21 + {#each pinned as repo (repo.rkey)} 22 + <RepoCard {repo} starButton={false} showOwner={false} /> 23 + {/each} 24 + </Section> 25 + </div> 27 26 28 27 <section class="order-2 md:col-span-4"> 29 28 <h2 class="px-2 pb-4 text-base font-medium">Activity</h2>
+6 -13
web/src/lib/components/profile/tabs/PeopleTab.svelte
··· 1 1 <script lang="ts"> 2 2 import FollowCard from "../FollowCard.svelte"; 3 - import EmptyState from "../EmptyState.svelte"; 3 + import Section from "$lib/components/ui/Section.svelte"; 4 4 import type { PersonData } from "../types"; 5 5 6 6 interface Props { ··· 12 12 let { people, title, emptyMessage }: Props = $props(); 13 13 </script> 14 14 15 - <section> 16 - <h2 class="px-2 pb-4 text-base font-medium">{title}</h2> 17 - {#if people.length === 0} 18 - <EmptyState message={emptyMessage} /> 19 - {:else} 20 - <div class="flex flex-col gap-8"> 21 - {#each people as person (person.did)} 22 - <FollowCard {person} /> 23 - {/each} 24 - </div> 25 - {/if} 26 - </section> 15 + <Section {title} empty={people.length === 0} {emptyMessage} listClass="flex flex-col gap-8"> 16 + {#each people as person (person.did)} 17 + <FollowCard {person} /> 18 + {/each} 19 + </Section>
+35 -39
web/src/lib/components/profile/tabs/RepoListTab.svelte
··· 2 2 import { page } from "$app/state"; 3 3 import { resolve } from "$app/paths"; 4 4 import RepoCard from "$lib/components/repo/RepoCard.svelte"; 5 - import EmptyState from "../EmptyState.svelte"; 5 + import Section from "$lib/components/ui/Section.svelte"; 6 6 import type { RepoCardData } from "../types"; 7 7 import Search from "$icon/search"; 8 8 import X from "$icon/x"; ··· 11 11 let searchQuery = $derived(page.url.searchParams.get("q") ?? ""); 12 12 </script> 13 13 14 - <section> 15 - <div class="mb-4"> 16 - <form class="relative flex" method="GET"> 17 - <input type="hidden" name="tab" value="repos" /> 18 - <div class="relative flex flex-1"> 19 - <input 20 - class="peer flex-1 rounded-l border border-border-default bg-background-default py-1.5 pl-3 pr-10 outline-none focus:border-border-strong focus:ring-1 focus:ring-border-strong" 21 - type="text" 22 - name="q" 23 - value={searchQuery} 24 - placeholder="Search repositories..." 25 - /> 26 - <a 27 - href={resolve(`${page.url.pathname}?tab=repos` as "/")} 28 - class="absolute right-3 top-1/2 hidden -translate-y-1/2 text-foreground-subtle hover:text-foreground-default peer-[&:not(:placeholder-shown)]:block" 14 + <Section 15 + empty={repos.length === 0} 16 + emptyMessage={searchQuery ? `No repositories match "${searchQuery}".` : "No repositories found."} 17 + > 18 + {#snippet header()} 19 + <div class="mb-4"> 20 + <form class="relative flex" method="GET"> 21 + <input type="hidden" name="tab" value="repos" /> 22 + <div class="relative flex flex-1"> 23 + <input 24 + class="peer flex-1 rounded-l border border-border-default bg-background-default py-1.5 pr-10 pl-3 outline-none focus:border-border-strong focus:ring-1 focus:ring-border-strong" 25 + type="text" 26 + name="q" 27 + value={searchQuery} 28 + placeholder="Search repositories..." 29 + /> 30 + <a 31 + href={resolve(`${page.url.pathname}?tab=repos` as "/")} 32 + class="absolute top-1/2 right-3 hidden -translate-y-1/2 text-foreground-subtle peer-[&:not(:placeholder-shown)]:block hover:text-foreground-default" 33 + > 34 + <X class="size-4" /> 35 + </a> 36 + </div> 37 + <button 38 + type="submit" 39 + class="rounded-r border border-l-0 border-border-default bg-background-default p-2 text-foreground-subtle hover:bg-background-subtle hover:text-foreground-default" 29 40 > 30 - <X class="size-4" /> 31 - </a> 32 - </div> 33 - <button 34 - type="submit" 35 - class="rounded-r border border-l-0 border-border-default bg-background-default p-2 text-foreground-subtle hover:bg-background-subtle hover:text-foreground-default" 36 - > 37 - <Search class="size-4" /> 38 - </button> 39 - </form> 40 - </div> 41 - 42 - {#if repos.length === 0} 43 - <EmptyState 44 - message={searchQuery ? `No repositories match "${searchQuery}".` : "No repositories found."} 45 - /> 46 - {:else} 47 - <div class="flex flex-col gap-4"> 48 - {#each repos as repo (repo.rkey)} 49 - <RepoCard {repo} showOwner={false} /> 50 - {/each} 41 + <Search class="size-4" /> 42 + </button> 43 + </form> 51 44 </div> 52 - {/if} 53 - </section> 45 + {/snippet} 46 + {#each repos as repo (repo.rkey)} 47 + <RepoCard {repo} showOwner={false} /> 48 + {/each} 49 + </Section>
+19 -26
web/src/lib/components/profile/tabs/StarredTab.svelte
··· 1 1 <script lang="ts"> 2 2 import { resolve } from "$app/paths"; 3 3 import RepoCard from "$lib/components/repo/RepoCard.svelte"; 4 - import EmptyState from "../EmptyState.svelte"; 4 + import Section from "$lib/components/ui/Section.svelte"; 5 5 import type { StarData } from "../types"; 6 6 7 7 let { stars }: { stars: StarData[] } = $props(); 8 8 </script> 9 9 10 - <section> 11 - <h2 class="px-2 pb-4 text-base font-medium">Starred</h2> 12 - {#if stars.length === 0} 13 - <EmptyState message="No stars yet." /> 14 - {:else} 15 - <div class="flex flex-col gap-4"> 16 - {#each stars as star (star.uri)} 17 - {#if star.kind === "repo"} 18 - <RepoCard repo={star.repo} /> 19 - {:else} 20 - <div 21 - class="flex items-center gap-2 rounded border border-border-default bg-background-default px-6 py-4 shadow-sm" 22 - > 23 - <a 24 - href={resolve(`/${star.ownerHandle}/strings/${star.rkey}` as "/")} 25 - class="truncate font-bold text-foreground-default no-underline hover:underline" 26 - > 27 - {star.ownerHandle}/{star.rkey} 28 - </a> 29 - </div> 30 - {/if} 31 - {/each} 32 - </div> 33 - {/if} 34 - </section> 10 + <Section title="Starred" empty={stars.length === 0} emptyMessage="No stars yet."> 11 + {#each stars as star (star.uri)} 12 + {#if star.kind === "repo"} 13 + <RepoCard repo={star.repo} /> 14 + {:else} 15 + <div 16 + class="flex items-center gap-2 rounded border border-border-default bg-background-default px-6 py-4 shadow-sm" 17 + > 18 + <a 19 + href={resolve(`/${star.ownerHandle}/strings/${star.rkey}` as "/")} 20 + class="truncate font-bold text-foreground-default no-underline hover:underline" 21 + > 22 + {star.ownerHandle}/{star.rkey} 23 + </a> 24 + </div> 25 + {/if} 26 + {/each} 27 + </Section>
+6 -13
web/src/lib/components/profile/tabs/StringListTab.svelte
··· 1 1 <script lang="ts"> 2 2 import StringCard from "../StringCard.svelte"; 3 - import EmptyState from "../EmptyState.svelte"; 3 + import Section from "$lib/components/ui/Section.svelte"; 4 4 import type { StringCardData } from "../types"; 5 5 6 6 let { strings }: { strings: StringCardData[] } = $props(); 7 7 </script> 8 8 9 - <section> 10 - <h2 class="px-2 pb-4 text-base font-medium">Strings</h2> 11 - {#if strings.length === 0} 12 - <EmptyState message="No strings yet." /> 13 - {:else} 14 - <div class="flex flex-col gap-4"> 15 - {#each strings as entry (entry.rkey)} 16 - <StringCard {entry} /> 17 - {/each} 18 - </div> 19 - {/if} 20 - </section> 9 + <Section title="Strings" empty={strings.length === 0} emptyMessage="No strings yet."> 10 + {#each strings as entry (entry.rkey)} 11 + <StringCard {entry} /> 12 + {/each} 13 + </Section>
+12 -14
web/src/lib/components/profile/tabs/VouchTab.svelte
··· 1 1 <script lang="ts"> 2 2 import VouchCard from "../VouchCard.svelte"; 3 - import EmptyState from "../EmptyState.svelte"; 3 + import Section from "$lib/components/ui/Section.svelte"; 4 4 import type { VouchData } from "../types"; 5 5 6 6 let { ··· 12 12 const profileLabel = $derived(isSelf ? "you" : profileHandle); 13 13 </script> 14 14 15 - <section> 16 - <h2 class="px-2 pb-4 text-base font-medium">Vouches</h2> 17 - {#if vouches.length === 0} 18 - <EmptyState message="No vouches yet." /> 19 - {:else} 20 - <div class="relative ml-5 flex flex-col gap-6 border border-transparent px-4"> 21 - <div class="absolute bottom-8 top-8 z-0 w-0.5 bg-border-default"></div> 22 - {#each vouches as vouch (vouch.uri)} 23 - <VouchCard {vouch} {profileLabel} /> 24 - {/each} 25 - </div> 26 - {/if} 27 - </section> 15 + <Section 16 + title="Vouches" 17 + empty={vouches.length === 0} 18 + emptyMessage="No vouches yet." 19 + listClass="relative ml-5 flex flex-col gap-6 border border-transparent px-4" 20 + > 21 + <div class="absolute top-8 bottom-8 z-0 w-0.5 bg-border-default"></div> 22 + {#each vouches as vouch (vouch.uri)} 23 + <VouchCard {vouch} {profileLabel} /> 24 + {/each} 25 + </Section>
+1 -1
web/src/lib/components/repo/RepoCard.svelte
··· 27 27 class="flex min-h-32 flex-col gap-1 rounded border border-border-default bg-background-default px-6 py-4 shadow-sm" 28 28 > 29 29 <div class="flex items-start justify-between font-medium text-foreground-default"> 30 - <div class="mr-2 flex min-w-0 flex-1 items-center mt-1"> 30 + <div class="mt-1 mr-2 flex min-w-0 flex-1 items-center"> 31 31 <BookMarked class="mr-1.5 size-4 shrink-0 text-foreground-subtle" aria-hidden="true" /> 32 32 <a 33 33 href={resolve(`/${repo.ownerHandle}/${repo.name}` as "/")}
+5 -5
web/src/lib/components/shell/Topbar.svelte
··· 38 38 39 39 {#snippet divider(classes = "")} 40 40 <div 41 - class={["hidden h-6 w-px rounded-md bg-background-inset self-center md:block", classes]} 41 + class={["hidden h-6 w-px self-center rounded-md bg-background-inset md:block", classes]} 42 42 ></div> 43 43 {/snippet} 44 44 45 45 <nav 46 - class={`h-11 w-full px-5 \ 47 - ${variant === "marketing" ? "" : "bg-background-default border-b border-border-default shadow-[0_1px_1px_0_rgba(0,0,0,0.05)]"} \ 46 + class={`h-11 w-full px-5 47 + ${variant === "marketing" ? "" : "border-b border-border-default bg-background-default shadow-[0_1px_1px_0_rgba(0,0,0,0.05)]"} 48 48 `} 49 49 aria-label="main" 50 50 > ··· 84 84 </div> 85 85 86 86 <div class="order-1 flex items-center md:order-3"> 87 - <Dropdown id="new-menu" group="topbar" variant="button"> 87 + <Dropdown id="new-menu" group="topbar" variant="button" align="right"> 88 88 {#snippet trigger()} 89 89 <Plus class="size-3.5" aria-hidden="true" /> 90 90 <span class="hidden md:inline">New</span> ··· 125 125 ></span> 126 126 </div> 127 127 {:else if user} 128 - <Dropdown id="profile-menu" group="topbar"> 128 + <Dropdown id="profile-menu" group="topbar" align="right"> 129 129 {#snippet trigger()} 130 130 {#if user.avatar} 131 131 <img
+1 -2
web/src/lib/components/ui/Avatar.svelte
··· 13 13 let failed = $state(false); 14 14 15 15 $effect(() => { 16 - src; 17 - failed = false; 16 + if (src) failed = false; 18 17 }); 19 18 </script> 20 19
+4 -4
web/src/lib/components/ui/Button.svelte
··· 3 3 4 4 export const button = tv({ 5 5 base: [ 6 - "inline-flex cursor-pointer select-none items-center justify-center overflow-hidden rounded-l-(--btn-radius-l) rounded-r-(--btn-radius-r) [--btn-radius:var(--radius-sm)] [--btn-radius-l:var(--btn-radius)] [--btn-radius-r:var(--btn-radius)] no-underline", 6 + "inline-flex cursor-pointer items-center justify-center overflow-hidden rounded-l-(--btn-radius-l) rounded-r-(--btn-radius-r) no-underline select-none [--btn-radius-l:var(--btn-radius)] [--btn-radius-r:var(--btn-radius)] [--btn-radius:var(--radius-sm)]", 7 7 "transition-colors duration-150 ease-in-out", 8 8 "hover:no-underline", 9 9 "focus-visible:outline-2 focus-visible:outline-offset-2", ··· 22 22 "focus-visible:outline-background-success-emphasis" 23 23 ], 24 24 ghost: [ 25 - "bg-transparent text-foreground-muted border border-transparent", 25 + "border border-transparent bg-transparent text-foreground-muted", 26 26 "hover:bg-background-inset", 27 27 "focus-visible:outline-border-focus" 28 28 ], ··· 60 60 variant: "default", 61 61 insetShadow: true, 62 62 class: [ 63 - "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:content-[''] before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150", 63 + "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150 before:content-['']", 64 64 "hover:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-strong-hover)]", 65 65 "active:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)]", 66 66 "disabled:active:translate-y-0" ··· 70 70 variant: "primary", 71 71 insetShadow: true, 72 72 class: [ 73 - "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:content-[''] before:shadow-[inset_0_-2px_0_0_var(--color-alpha-dark-25)] before:transition-all before:duration-150", 73 + "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:shadow-[inset_0_-2px_0_0_var(--color-alpha-dark-25)] before:transition-all before:duration-150 before:content-['']", 74 74 "active:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-alpha-dark-50)]", 75 75 "disabled:active:translate-y-0" 76 76 ]
+4 -7
web/src/lib/components/ui/ButtonCard.svelte
··· 3 3 4 4 export const buttonCard = tv({ 5 5 base: [ 6 - "flex h-40 w-48 cursor-pointer gap-3 rounded-(--btn-radius) [--btn-radius:var(--radius-sm)] px-4 py-3 select-none no-underline", 6 + "flex h-40 w-48 cursor-pointer gap-3 rounded-(--btn-radius) px-4 py-3 no-underline select-none [--btn-radius:var(--radius-sm)]", 7 7 "transition-colors duration-150 ease-in-out", 8 8 "hover:no-underline", 9 9 "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus", ··· 15 15 "border border-border-default bg-background-default", 16 16 "hover:bg-background-subtle" 17 17 ], 18 - ghost: [ 19 - "border border-transparent bg-transparent", 20 - "hover:bg-background-inset" 21 - ] 18 + ghost: ["border border-transparent bg-transparent", "hover:bg-background-inset"] 22 19 }, 23 20 insetShadow: { 24 21 true: "", ··· 31 28 variant: "default", 32 29 insetShadow: true, 33 30 class: [ 34 - "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-[max(calc(var(--btn-radius)-2px),0px)] before:content-[''] before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150", 31 + "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-[max(calc(var(--btn-radius)-2px),0px)] before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150 before:content-['']", 35 32 "hover:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-strong-hover)]", 36 33 "active:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)]", 37 34 "disabled:active:translate-y-0" ··· 90 87 {#snippet content()} 91 88 <div class="flex flex-col items-start gap-1.5"> 92 89 {#if title} 93 - <div class="flex flex-row gap-2 items-center"> 90 + <div class="flex flex-row items-center gap-2"> 94 91 {#if Icon} 95 92 <Icon class="size-4 shrink-0" aria-hidden="true" /> 96 93 {/if}
+2 -2
web/src/lib/components/ui/ButtonGroup.svelte
··· 9 9 false: [ 10 10 "gap-0", 11 11 "*:relative", 12 - "[&>*:not(:first-child)]:[--btn-radius-l:0px] [&>*:not(:first-child)]:-ml-px", 12 + "[&>*:not(:first-child)]:-ml-px [&>*:not(:first-child)]:[--btn-radius-l:0px]", 13 13 "[&>*:not(:last-child)]:[--btn-radius-r:0px]", 14 - "[&>*:hover]:z-10 [&>*:focus-visible]:z-10" 14 + "[&>*:focus-visible]:z-10 [&>*:hover]:z-10" 15 15 ] 16 16 } 17 17 },
+2 -8
web/src/lib/components/ui/Checkbox.svelte
··· 58 58 class={inputClasses} 59 59 {...rest} 60 60 /> 61 - <Check 62 - class={checkClasses} 63 - aria-hidden="true" 64 - /> 65 - <Minus 66 - class={minusClasses} 67 - aria-hidden="true" 68 - /> 61 + <Check class={checkClasses} aria-hidden="true" /> 62 + <Minus class={minusClasses} aria-hidden="true" /> 69 63 </span> 70 64 {#if children} 71 65 <span>{@render children()}</span>
+11
web/src/lib/components/ui/Dropdown.stories.svelte
··· 54 54 </Dropdown> 55 55 </Story> 56 56 57 + <!-- `align="right"` anchors the menu's right edge to the trigger, for triggers near 58 + the right edge of the viewport (e.g. the topbar profile menu). Default is left. --> 59 + <Story name="Right aligned" asChild> 60 + <div class="flex justify-end"> 61 + <Dropdown align="right" trigger={menuTrigger} label="Menu"> 62 + <DropdownItem icon={Pencil}>Edit</DropdownItem> 63 + <DropdownItem icon={Settings}>Settings</DropdownItem> 64 + </Dropdown> 65 + </div> 66 + </Story> 67 + 57 68 <!-- DropdownItem states: plain text, with icon, as a link (href), and the danger variant. --> 58 69 <Story name="Item states" asChild> 59 70 <Dropdown trigger={menuTrigger} label="Menu">
+13 -3
web/src/lib/components/ui/Dropdown.svelte
··· 7 7 root: "relative inline-block", 8 8 plainTrigger: 9 9 "flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-inherit", 10 - menu: "absolute right-0 z-50 mt-2 w-56 overflow-hidden rounded border border-border-default bg-background-default text-sm shadow-menu" 10 + menu: "absolute z-50 mt-2 w-56 divide-y divide-border-default overflow-hidden rounded border border-border-default bg-background-default text-sm shadow-menu" 11 11 }, 12 12 variants: { 13 + align: { 14 + left: { 15 + menu: "left-0" 16 + }, 17 + right: { 18 + menu: "right-0" 19 + } 20 + }, 13 21 open: { 14 22 true: { 15 23 menu: "block" ··· 20 28 } 21 29 }, 22 30 defaultVariants: { 31 + align: "left", 23 32 open: false 24 33 } 25 34 }); ··· 60 69 id?: string; 61 70 group?: string; 62 71 variant?: "plain" | "button"; 72 + align?: DropdownVariants["align"]; 63 73 label?: string; 64 74 trigger: Snippet; 65 75 children: Snippet; 66 76 } 67 77 68 - let { id, group, variant = "plain", label, trigger, children }: Props = $props(); 78 + let { id, group, variant = "plain", align = "left", label, trigger, children }: Props = $props(); 69 79 70 80 let open = $state(false); 71 - const classes = $derived(dropdown({ open })); 81 + const classes = $derived(dropdown({ open, align })); 72 82 let root = $state<HTMLElement>(); 73 83 let triggerWrapper = $state<HTMLElement>(); 74 84 let triggerButtonEl = $state<HTMLElement>();
+1 -1
web/src/lib/components/ui/DropdownItem.svelte
··· 2 2 import { tv, type VariantProps } from "tailwind-variants"; 3 3 4 4 export const dropdownItem = tv({ 5 - base: "flex w-full cursor-pointer items-center gap-2 border-0 bg-transparent px-[0.7rem] py-[0.55rem] text-left text-inherit no-underline transition-colors duration-150 hover:no-underline", 5 + base: "flex w-full cursor-pointer items-center gap-2 bg-transparent px-[0.7rem] py-[0.55rem] text-left text-inherit no-underline transition-colors duration-150 hover:no-underline", 6 6 variants: { 7 7 danger: { 8 8 false: "hover:bg-background-subtle dark:hover:bg-[rgb(55_65_81_/_0.5)]",
+23
web/src/lib/components/ui/EmptyState.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import EmptyState from "./EmptyState.svelte"; 4 + import Link from "./Link.svelte"; 5 + 6 + const { Story } = defineMeta({ 7 + title: "UI/EmptyState", 8 + component: EmptyState, 9 + tags: ["autodocs"], 10 + args: { 11 + message: "No repositories yet." 12 + } 13 + }); 14 + </script> 15 + 16 + <Story name="Default" /> 17 + 18 + <!-- Children render inline after the message — typically a call-to-action link. --> 19 + <Story name="With action" asChild> 20 + <EmptyState message="No repositories yet."> 21 + <Link href="/repo/new">Create one</Link> 22 + </EmptyState> 23 + </Story>
+1 -1
web/src/lib/components/ui/Input.svelte
··· 2 2 import { tv, type VariantProps } from "tailwind-variants"; 3 3 4 4 export const inputField = tv({ 5 - base: "flex items-center gap-2 rounded border bg-background-default px-2 py-1 min-h-9 transition-colors", 5 + base: "flex min-h-9 items-center gap-2 rounded border bg-background-default px-2 py-1 transition-colors", 6 6 variants: { 7 7 error: { 8 8 false:
+2 -2
web/src/lib/components/ui/Link.svelte
··· 2 2 import { tv, type VariantProps } from "tailwind-variants"; 3 3 4 4 export const link = tv({ 5 - base: "inline-flex cursor-pointer select-none items-center gap-1 no-underline transition-colors duration-150 ease-in-out hover:underline focus-visible:outline-2 focus-visible:outline-offset-2", 5 + base: "inline-flex cursor-pointer items-center gap-1 no-underline transition-colors duration-150 ease-in-out select-none hover:underline focus-visible:outline-2 focus-visible:outline-offset-2", 6 6 variants: { 7 7 variant: { 8 8 default: ··· 15 15 "text-foreground-link-danger hover:text-foreground-link-danger-hover focus-visible:outline-foreground-link-danger" 16 16 }, 17 17 size: { 18 - regular: "text-sm", 18 + regular: "", 19 19 small: "text-xs" 20 20 }, 21 21 disabled: {
+1 -1
web/src/lib/components/ui/Logo.stories.svelte
··· 39 39 </Story> 40 40 41 41 <Story name="On Emphasis" asChild> 42 - <div class="bg-background-success-emphasis flex flex-col items-start gap-4 rounded p-4"> 42 + <div class="flex flex-col items-start gap-4 rounded bg-background-success-emphasis p-4"> 43 43 <Logo class="text-foreground-on-emphasis" /> 44 44 <Logo wordmark class="text-foreground-on-emphasis" /> 45 45 </div>
+1 -1
web/src/lib/components/ui/Logo.svelte
··· 73 73 <span class="inline-flex items-center gap-2 select-none"> 74 74 {@render mark()} 75 75 <span 76 - class="rounded bg-background-subtle px-1 text-[10px] leading-4 font-normal not-italic text-foreground-default" 76 + class="rounded bg-background-subtle px-1 text-[10px] leading-4 font-normal text-foreground-default not-italic" 77 77 > 78 78 alpha 79 79 </span>
+1 -1
web/src/lib/components/ui/Pagination.svelte
··· 97 97 </Button> 98 98 {:else} 99 99 <span 100 - class="flex size-8 items-center justify-center text-foreground-subtle pointer-events-none select-none" 100 + class="pointer-events-none flex size-8 items-center justify-center text-foreground-subtle select-none" 101 101 aria-hidden="true" 102 102 > 103 103 &hellip;
+9 -9
web/src/lib/components/ui/Radio.svelte
··· 31 31 {value} 32 32 bind:group 33 33 {disabled} 34 - class="peer size-4 shrink-0 appearance-none rounded-full cursor-pointer border border-border-strong bg-background-default transition-colors duration-150 \ 35 - hover:bg-background-subtle \ 36 - checked:border-foreground-default \ 37 - focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground-default \ 38 - disabled:cursor-not-allowed disabled:pointer-events-none disabled:border-border-disabled disabled:hover:bg-inherit" 34 + class="peer \ \ \ \ size-4 shrink-0 cursor-pointer appearance-none rounded-full border border-border-strong 35 + bg-background-default transition-colors 36 + duration-150 checked:border-foreground-default 37 + hover:bg-background-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground-default 38 + disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-border-disabled disabled:hover:bg-inherit" 39 39 {...rest} 40 40 /> 41 41 <span 42 - class="absolute inset-0 m-auto hidden size-2 rounded-full \ 43 - bg-foreground-default \ 44 - peer-checked:block \ 45 - peer-disabled:text-foreground-disabled peer-disabled:bg-foreground-disabled" 42 + class="\ \ \ absolute inset-0 m-auto hidden 43 + size-2 rounded-full 44 + bg-foreground-default peer-checked:block 45 + peer-disabled:bg-foreground-disabled peer-disabled:text-foreground-disabled" 46 46 aria-hidden="true" 47 47 ></span> 48 48 </span>
+50
web/src/lib/components/ui/Section.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import Section from "./Section.svelte"; 4 + 5 + const { Story } = defineMeta({ 6 + title: "UI/Section", 7 + component: Section, 8 + tags: ["autodocs"] 9 + }); 10 + </script> 11 + 12 + {#snippet items()} 13 + <div class="rounded-sm border border-border-default p-4">First item</div> 14 + <div class="rounded-sm border border-border-default p-4">Second item</div> 15 + <div class="rounded-sm border border-border-default p-4">Third item</div> 16 + {/snippet} 17 + 18 + <Story name="Default" asChild> 19 + <Section title="Repositories" empty={false} emptyMessage="No repositories yet."> 20 + {@render items()} 21 + </Section> 22 + </Story> 23 + 24 + <!-- With `empty`, children are ignored and EmptyState renders with `emptyMessage`. --> 25 + <Story name="Empty" asChild> 26 + <Section title="Repositories" empty emptyMessage="No repositories yet."> 27 + {@render items()} 28 + </Section> 29 + </Story> 30 + 31 + <Story name="Without title" asChild> 32 + <Section empty={false} emptyMessage="Nothing here."> 33 + {@render items()} 34 + </Section> 35 + </Story> 36 + 37 + <!-- `header` renders between the title and the list; `listClass` overrides the list layout. --> 38 + <Story name="Header and custom list" asChild> 39 + <Section 40 + title="Pinned" 41 + empty={false} 42 + emptyMessage="Nothing pinned." 43 + listClass="grid grid-cols-2 gap-4" 44 + > 45 + {#snippet header()} 46 + <p class="px-2 pb-2 text-sm text-foreground-subtle">Up to four pinned repositories.</p> 47 + {/snippet} 48 + {@render items()} 49 + </Section> 50 + </Story>
+36
web/src/lib/components/ui/Section.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from "svelte"; 3 + import EmptyState from "./EmptyState.svelte"; 4 + 5 + interface Props { 6 + title?: string; 7 + empty: boolean; 8 + emptyMessage: string; 9 + listClass?: string; 10 + header?: Snippet; 11 + children: Snippet; 12 + } 13 + 14 + let { 15 + title, 16 + empty, 17 + emptyMessage, 18 + listClass = "flex flex-col gap-4", 19 + header, 20 + children 21 + }: Props = $props(); 22 + </script> 23 + 24 + <section> 25 + {#if title} 26 + <h2 class="px-2 pb-4 text-base font-medium">{title}</h2> 27 + {/if} 28 + {@render header?.()} 29 + {#if empty} 30 + <EmptyState message={emptyMessage} /> 31 + {:else} 32 + <div class={listClass}> 33 + {@render children()} 34 + </div> 35 + {/if} 36 + </section>
+2 -2
web/src/lib/components/ui/Select.svelte
··· 2 2 import { tv, type VariantProps } from "tailwind-variants"; 3 3 4 4 export const select = tv({ 5 - base: "w-full appearance-none rounded border bg-background-default px-3 py-2 pr-9 text-sm outline-none transition-colors disabled:cursor-not-allowed", 5 + base: "w-full appearance-none rounded border bg-background-default px-3 py-2 pr-9 text-sm transition-colors outline-none disabled:cursor-not-allowed", 6 6 variants: { 7 7 error: { 8 8 false: ··· 31 31 export type SelectVariants = VariantProps<typeof select>; 32 32 33 33 const chevron = tv({ 34 - base: "pointer-events-none absolute right-3 top-1/2 size-3.5 -translate-y-1/2 text-foreground-subtle", 34 + base: "pointer-events-none absolute top-1/2 right-3 size-3.5 -translate-y-1/2 text-foreground-subtle", 35 35 variants: { 36 36 error: { 37 37 true: "text-foreground-danger"
+32
web/src/lib/components/ui/Tabs.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import Tabs from "./Tabs.svelte"; 4 + import BookMarked from "$icon/book-marked"; 5 + import Star from "$icon/star"; 6 + import SquareChartGantt from "$icon/square-chart-gantt"; 7 + 8 + const tabs = [ 9 + { id: "overview", label: "Overview", icon: SquareChartGantt, href: "#" }, 10 + { id: "repos", label: "Repositories", icon: BookMarked, count: 12, href: "#" }, 11 + { id: "starred", label: "Starred", icon: Star, count: 3, href: "#" } 12 + ]; 13 + 14 + const plainTabs = [ 15 + { id: "open", label: "Open", count: 4, href: "#" }, 16 + { id: "closed", label: "Closed", count: 28, href: "#" } 17 + ]; 18 + 19 + const { Story } = defineMeta({ 20 + title: "UI/Tabs", 21 + component: Tabs, 22 + tags: ["autodocs"], 23 + args: { 24 + tabs, 25 + active: "overview" 26 + } 27 + }); 28 + </script> 29 + 30 + <Story name="Default" /> 31 + <Story name="SecondActive" args={{ active: "repos" }} /> 32 + <Story name="WithoutIcons" args={{ tabs: plainTabs, active: "open" }} />
+56
web/src/lib/components/ui/Tabs.svelte
··· 1 + <script module lang="ts"> 2 + import type { Component } from "svelte"; 3 + import type { SvelteHTMLElements } from "svelte/elements"; 4 + 5 + export interface TabDef { 6 + id: string; 7 + label: string; 8 + href: string; 9 + icon?: Component<SvelteHTMLElements["svg"]>; 10 + count?: number; 11 + } 12 + </script> 13 + 14 + <script lang="ts"> 15 + import { resolve } from "$app/paths"; 16 + 17 + interface Props { 18 + tabs: TabDef[]; 19 + active: string; 20 + label?: string; 21 + } 22 + 23 + let { tabs, active, label }: Props = $props(); 24 + </script> 25 + 26 + <nav class="w-full overflow-x-auto overflow-y-hidden pl-4" aria-label={label}> 27 + <div class="flex"> 28 + {#each tabs as tab (tab.id)} 29 + {@const isActive = active === tab.id} 30 + {@const Glyph = tab.icon} 31 + <a 32 + href={resolve(tab.href as "/")} 33 + class="group relative -mr-px no-underline hover:no-underline" 34 + aria-current={isActive ? "page" : undefined} 35 + > 36 + <div 37 + class={`relative mr-1 min-w-[80px] rounded-t px-4 py-1 text-center whitespace-nowrap text-foreground-default ${ 38 + isActive 39 + ? "-mb-px bg-background-default font-medium" 40 + : "group-hover:bg-background-inset/25" 41 + }`} 42 + > 43 + <span class="flex items-center justify-center"> 44 + {#if Glyph} 45 + <Glyph class="mr-2 h-4 w-4" aria-hidden="true" /> 46 + {/if} 47 + {tab.label} 48 + {#if tab.count} 49 + <span class="ml-1 rounded bg-background-inset px-1 text-sm">{tab.count}</span> 50 + {/if} 51 + </span> 52 + </div> 53 + </a> 54 + {/each} 55 + </div> 56 + </nav>
+1 -1
web/src/lib/components/ui/Tag.svelte
··· 2 2 import { tv, type VariantProps } from "tailwind-variants"; 3 3 4 4 export const tag = tv({ 5 - base: "inline-flex select-none items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium leading-none", 5 + base: "inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs leading-none font-medium select-none", 6 6 variants: { 7 7 color: { 8 8 default: "border border-border-default bg-background-default text-foreground-default",
+3 -3
web/src/lib/components/ui/Textarea.svelte
··· 80 80 <div class={root({ class: className })}> 81 81 {#if iconLeft} 82 82 {@const IconLeft = iconLeft} 83 - <IconLeft class="size-4 mt-0.5 mr-1 shrink-0 text-foreground-subtle" aria-hidden="true" /> 83 + <IconLeft class="mt-0.5 mr-1 size-4 shrink-0 text-foreground-subtle" aria-hidden="true" /> 84 84 {/if} 85 85 <textarea 86 86 bind:value ··· 91 91 class={textarea({ class: loading ? "pr-5" : undefined })} 92 92 {...rest}></textarea> 93 93 {#if loading} 94 - <Spinner class="absolute right-2.5 top-2 size-4" /> 94 + <Spinner class="absolute top-2 right-2.5 size-4" /> 95 95 {:else if iconRight} 96 96 {@const IconRight = iconRight} 97 - <IconRight class="size-4 mt-0.5 ml-1 shrink-0 text-foreground-subtle" aria-hidden="true" /> 97 + <IconRight class="mt-0.5 ml-1 size-4 shrink-0 text-foreground-subtle" aria-hidden="true" /> 98 98 {/if} 99 99 </div>
+1 -1
web/src/routes/[handle]/+layout.svelte
··· 2 2 import { page } from "$app/state"; 3 3 import { setContext } from "svelte"; 4 4 import { createProfileCounts, PROFILE_COUNTS_KEY } from "$lib/components/profile/counts.svelte"; 5 - import EmptyState from "$lib/components/profile/EmptyState.svelte"; 5 + import EmptyState from "$lib/components/ui/EmptyState.svelte"; 6 6 import ProfileCard from "$lib/components/profile/ProfileCard.svelte"; 7 7 import ProfileTabs from "$lib/components/profile/ProfileTabs.svelte"; 8 8
+2 -2
web/src/routes/login/+page.svelte
··· 43 43 <a href={appPath("/")} class="flex items-center gap-2 no-underline hover:no-underline"> 44 44 <img src="/logos/logotype.svg" alt="tangled" class="h-24 w-48 dark:invert" /> 45 45 </a> 46 - <p class="text-center text-xl italic text-foreground-default">tightly-knit social coding.</p> 46 + <p class="text-center text-xl text-foreground-default italic">tightly-knit social coding.</p> 47 47 </div> 48 48 49 49 <form class="mt-4" onsubmit={submit}> ··· 60 60 inputmode="url" 61 61 required 62 62 placeholder="akshay.tngl.sh" 63 - class="w-full rounded border border-border-default bg-background-default p-3 text-lg text-foreground-default placeholder:text-foreground-placeholder focus:outline-none focus:ring-1 focus:ring-border-strong md:text-base" 63 + class="w-full rounded border border-border-default bg-background-default p-3 text-lg text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none md:text-base" 64 64 /> 65 65 <p class="mt-1 text-sm text-foreground-subtle"> 66 66 Use your <span class="text-foreground-default">AT Protocol</span> handle to log in. If
+35 -28
web/src/routes/repo/new/+page.svelte
··· 1 1 <script lang="ts"> 2 + import { resolve } from "$app/paths"; 2 3 import { getAuth } from "$lib/auth.svelte"; 3 4 import Avatar from "$lib/components/ui/Avatar.svelte"; 4 5 import Button from "$lib/components/ui/Button.svelte"; ··· 23 24 <div class="mx-auto w-full max-w-2xl px-4 py-12"> 24 25 <div class="px-6 py-2"> 25 26 <h1 class="text-xl font-bold text-foreground-default">Create a new repository</h1> 26 - <p class="text-sm text-foreground-subtle mb-1"> 27 + <p class="mb-1 text-sm text-foreground-subtle"> 27 28 Repositories contain a project's files and version history. All repositories are publicly 28 29 accessible. 29 30 </p> 30 31 </div> 31 32 32 - <div class="bg-background-default drop-shadow-sm rounded p-6"> 33 + <div class="rounded bg-background-default p-6 drop-shadow-sm"> 33 34 <form> 34 - <div class="flex gap-4 relative border-l border-border-default pl-6"> 35 - <div class="absolute -left-3 -top-0"> 36 - <div class="size-6 bg-background-inset rounded-full flex items-center justify-center text-sm font-medium mt-1 text-foreground-muted"> 35 + <div class="relative flex gap-4 border-l border-border-default pl-6"> 36 + <div class="absolute -top-0 -left-3"> 37 + <div 38 + class="mt-1 flex size-6 items-center justify-center rounded-full bg-background-inset text-sm font-medium text-foreground-muted" 39 + > 37 40 1 38 41 </div> 39 42 </div> 40 43 41 44 <div class="flex-1 pb-12"> 42 45 <h2 class="text-lg font-medium text-foreground-default">General</h2> 43 - <div class="text-sm text-foreground-subtle mb-4">Basic repository information.</div> 46 + <div class="mb-4 text-sm text-foreground-subtle">Basic repository information.</div> 44 47 45 48 <div class="space-y-2"> 46 49 <div> 47 - <label class="block text-sm font-bold text-foreground-default mb-1"> 50 + <label class="mb-1 block text-sm font-bold text-foreground-default"> 48 51 Repository name 49 52 </label> 50 - <div class="flex flex-col md:flex-row md:items-center gap-2 md:gap-0 w-full"> 51 - <div class="shrink-0 hidden md:flex items-center px-2 py-2 gap-1 text-sm text-foreground-muted md:border md:border-r-0 md:border-border-default md:rounded-l md:bg-background-inset"> 53 + <div class="flex w-full flex-col gap-2 md:flex-row md:items-center md:gap-0"> 54 + <div 55 + class="hidden shrink-0 items-center gap-1 px-2 py-2 text-sm text-foreground-muted md:flex md:rounded-l md:border md:border-r-0 md:border-border-default md:bg-background-inset" 56 + > 52 57 <Avatar src={user?.avatar} handle={user?.handle} size="size-5" /> 53 58 <span>{user?.handle ?? "…"}</span> 54 59 </div> ··· 59 64 placeholder="repository-name" 60 65 bind:value={repoName} 61 66 required 62 - class="flex-1 md:rounded-r md:rounded-l-none py-2 rounded border border-border-default bg-background-default text-sm text-foreground-default placeholder:text-foreground-placeholder focus:outline-none focus:ring-1 focus:ring-border-strong px-2" 67 + class="flex-1 rounded border border-border-default bg-background-default px-2 py-2 text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none md:rounded-l-none md:rounded-r" 63 68 /> 64 69 </div> 65 - <p class="text-sm text-foreground-subtle mt-1"> 70 + <p class="mt-1 text-sm text-foreground-subtle"> 66 71 Choose a unique, descriptive name for your repository. Use letters, numbers, and 67 72 hyphens. 68 73 </p> 69 74 </div> 70 75 71 76 <div> 72 - <label for="description" class="block text-sm font-bold text-foreground-default mb-1"> 77 + <label for="description" class="mb-1 block text-sm font-bold text-foreground-default"> 73 78 Description 74 79 </label> 75 80 <input ··· 79 84 placeholder="A brief description of your project..." 80 85 maxlength={140} 81 86 bind:value={description} 82 - class="w-full py-2 px-2 rounded border border-border-default bg-background-default text-sm text-foreground-default placeholder:text-foreground-placeholder focus:outline-none focus:ring-1 focus:ring-border-strong" 87 + class="w-full rounded border border-border-default bg-background-default px-2 py-2 text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none" 83 88 /> 84 - <p class="text-sm text-foreground-subtle mt-1"> 89 + <p class="mt-1 text-sm text-foreground-subtle"> 85 90 Optional. A short description to help others understand what your project does (max 86 91 140 characters). 87 92 </p> ··· 90 95 </div> 91 96 </div> 92 97 93 - <div class="flex gap-4 relative border-l border-border-default pl-6"> 94 - <div class="absolute -left-3 -top-0"> 95 - <div class="size-6 bg-background-inset rounded-full flex items-center justify-center text-sm font-medium mt-1 text-foreground-muted"> 98 + <div class="relative flex gap-4 border-l border-border-default pl-6"> 99 + <div class="absolute -top-0 -left-3"> 100 + <div 101 + class="mt-1 flex size-6 items-center justify-center rounded-full bg-background-inset text-sm font-medium text-foreground-muted" 102 + > 96 103 2 97 104 </div> 98 105 </div> 99 106 100 107 <div class="flex-1"> 101 108 <h2 class="text-lg font-medium text-foreground-default">Configuration</h2> 102 - <div class="text-sm text-foreground-subtle mb-4">Repository settings and hosting.</div> 109 + <div class="mb-4 text-sm text-foreground-subtle">Repository settings and hosting.</div> 103 110 104 111 <div class="space-y-2"> 105 112 <div> 106 - <label for="branch" class="block text-sm font-bold text-foreground-default mb-1"> 113 + <label for="branch" class="mb-1 block text-sm font-bold text-foreground-default"> 107 114 Default branch 108 115 </label> 109 116 <input ··· 112 119 type="text" 113 120 bind:value={defaultBranch} 114 121 required 115 - class="w-full py-2 px-2 rounded border border-border-default bg-background-default text-sm text-foreground-default placeholder:text-foreground-placeholder focus:outline-none focus:ring-1 focus:ring-border-strong" 122 + class="w-full rounded border border-border-default bg-background-default px-2 py-2 text-sm text-foreground-default placeholder:text-foreground-placeholder focus:ring-1 focus:ring-border-strong focus:outline-none" 116 123 /> 117 - <p class="text-sm text-foreground-subtle mt-1"> 124 + <p class="mt-1 text-sm text-foreground-subtle"> 118 125 The primary branch where development happens. Common choices are "main" or "master". 119 126 </p> 120 127 </div> 121 128 122 129 <div> 123 - <label class="block text-sm font-bold text-foreground-default mb-1"> 130 + <label class="mb-1 block text-sm font-bold text-foreground-default"> 124 131 Select a knot 125 132 </label> 126 133 <div class="w-full space-y-2"> 127 - {#each knots as knot} 134 + {#each knots as knot (knot)} 128 135 <Radio value={knot} bind:group={selectedKnot} name="domain"> 129 136 {knot} 130 137 </Radio> 131 138 {/each} 132 139 </div> 133 - <p class="text-sm text-foreground-subtle mt-1"> 140 + <p class="mt-1 text-sm text-foreground-subtle"> 134 141 A knot hosts repository data and handles Git operations. You can also 135 - <a href="/settings/knots" class="underline">register your own knot</a>. 142 + <a href={resolve("/settings/knots" as "/")} class="underline" 143 + >register your own knot</a 144 + >. 136 145 </p> 137 146 </div> 138 147 </div> ··· 140 149 </div> 141 150 142 151 <div class="mt-8 flex justify-end"> 143 - <Button variant="primary" type="submit" icon={BookPlus}> 144 - Create repository (TODO) 145 - </Button> 152 + <Button variant="primary" type="submit" icon={BookPlus}>Create repository (TODO)</Button> 146 153 </div> 147 154 </form> 148 155 </div>
-1
web/vite.config.ts
··· 1 - /// <reference types="vitest/config" /> 2 1 import tailwindcss from "@tailwindcss/vite"; 3 2 import { sveltekit } from "@sveltejs/kit/vite"; 4 3 import Icons from "unplugin-icons/vite";