mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

at main 9.0 kB View raw
1<script setup lang="ts"> 2// If already signed in, bounce to the dashboard. 3const { data: session } = await useFetch('/api/me/whoami', { 4 // Don't blow up the page on the (very common) 401. 5 ignoreResponseError: true, 6}) 7 8if (session.value && typeof session.value === 'object' && 'did' in session.value) { 9 await navigateTo('/dashboard') 10} 11 12const installUrl = 'https://github.com/apps/synchub-to/installations/new' 13 14const heading = useTemplateRef('heading') 15const router = useRouter() 16router.afterEach(() => { 17 nextTick(() => heading.value?.focus({ focusVisible: false })) 18}) 19</script> 20 21<template> 22 <div class="shell"> 23 <a class="skip-link" href="#main">skip to main content</a> 24 <header class="nav-term"> 25 <nav class="nav-term__line" aria-label="primary"><span class="prompt" aria-hidden="true">&gt;</span> <a class="nav-term__mark" href="/"><SynchubMark :wordmark="true" :size="18" /></a> <ul class="nav-term__args"><li><a href="#start">--start</a></li> <li><a href="#signin">--sign-in</a></li></ul><span class="caret" aria-hidden="true"></span></nav> 26 </header> 27 28 <main id="main"> 29 <section class="hero reveal" style="--i: 0"> 30 <h1 ref="heading" tabindex="-1" class="hero__title heading-target">github tangled</h1> 31 <p class="hero__lede"> 32 synchub mirrors your public repositories to 33 <a href="https://tangled.org">tangled.org</a>. 34 install the github app, connect your tangled identity, and your branches 35 and tags will stay in sync. 36 </p> 37 38 <pre class="session" role="img" aria-label="How synchub works"><span class="c"># 1 · install the github app on the repos to mirror</span> 39<span class="p">$</span> gh app install <span class="a">synchub-to</span> 40 41<span class="c"># 2 · connect your tangled identity</span> 42<span class="p">$</span> synchub-to <span class="a">@alice.bsky.social</span> 43 44<span class="c"># 3 · push as usual</span> 45<span class="p">$</span> git push origin main 46<span class="ok"></span> mirrored alice/widget tangled.org <span class="dim">(2 refs)</span><span class="caret" aria-hidden="true"></span></pre> 47 </section> 48 49 <section id="start" class="panel reveal" style="--i: 1"> 50 <h2 class="panel__title">new here?</h2> 51 <p class="panel__body"> 52 install the github app on the repositories you'd like to mirror, 53 then come back and sign in with your tangled handle. (only public, 54 non-fork repos are synced.) 55 </p> 56 <a class="btn btn--primary" :href="installUrl">install the github app</a> 57 </section> 58 59 <section id="signin" class="panel reveal" style="--i: 2"> 60 <h2 class="panel__title">returning?</h2> 61 <p class="panel__body"> 62 already connected on another device? just enter your handle. 63 </p> 64 <form class="signin" action="/api/atproto/login" method="get"> 65 <div class="signin__group"> 66 <label class="signin__label" for="handle">handle</label> 67 <span class="signin__field"> 68 <span class="signin__prompt" aria-hidden="true">@</span> 69 <input 70 id="handle" 71 type="text" 72 name="handle" 73 required 74 placeholder="alice.bsky.social" 75 autocomplete="username" 76 autocapitalize="none" 77 autocorrect="off" 78 spellcheck="false" 79 > 80 </span> 81 </div> 82 <button class="btn" type="submit">sign in</button> 83 </form> 84 </section> 85 </main> 86 87 <footer id="how" class="foot-stmt"> 88 <p class="foot-stmt__line">code in sync.</p> 89 <div class="foot-stmt__meta"> 90 <SynchubMark :wordmark="true" :size="18" /> 91 <span class="muted">© 2026 · MIT · <a href="https://github.com/danielroe/synchub.to">source</a></span> 92 </div> 93 </footer> 94 </div> 95</template> 96 97<style scoped> 98.shell { 99 max-width: 52rem; 100 margin: 0 auto; 101 padding-inline: var(--page-gutter); 102} 103 104.nav-term { 105 padding: var(--space-md) 0; 106 border-bottom: var(--rule-hair) solid var(--color-rule); 107} 108 109.nav-term__line { 110 display: flex; 111 align-items: center; 112 flex-wrap: wrap; 113 gap: 0.6ch; 114 font-family: var(--font-mono); 115 font-size: var(--text-sm); 116 margin: 0; 117} 118 119.nav-term__line .prompt { color: var(--color-accent); } 120 121.nav-term__mark { 122 display: inline-flex; 123 text-decoration: none; 124 margin-right: 1.2ch; 125} 126 127.nav-term__mark:hover { color: inherit; } 128 129.nav-term__args { 130 display: inline-flex; 131 align-items: center; 132 gap: 0.9ch; 133 list-style: none; 134 margin: 0; 135 padding: 0; 136} 137 138.nav-term__args a { 139 display: inline-flex; 140 align-items: center; 141 min-height: 44px; 142 color: var(--color-muted); 143 text-decoration: underline; 144 text-underline-offset: 2px; 145 white-space: nowrap; 146} 147 148.nav-term__args a:hover { color: var(--color-ink); } 149 150.caret { 151 display: inline-block; 152 width: 0.55ch; 153 height: 1em; 154 margin-left: 0.15ch; 155 vertical-align: -0.12em; 156 background: var(--color-accent); 157 animation: blink 1.1s var(--ease-in-out) infinite; 158} 159 160@keyframes blink { 0%, 45% { opacity: 1; } 55%, 100% { opacity: 0; } } 161@media (prefers-reduced-motion: reduce) { .caret { animation: none; opacity: 1; } } 162 163.hero { 164 padding-block: var(--space-2xl) var(--space-xl); 165} 166 167.heading-target:focus { outline: none; } 168 169.hero__title { 170 font-size: var(--text-display); 171 line-height: 1.04; 172 margin: 0 0 var(--space-lg); 173 overflow-wrap: anywhere; 174 min-width: 0; 175} 176 177.hero__lede { 178 max-width: var(--measure); 179 color: var(--color-muted); 180 font-size: var(--text-md); 181 margin: 0 0 var(--space-xl); 182} 183 184.session { 185 font-family: var(--font-mono); 186 font-size: var(--text-sm); 187 line-height: 1.7; 188 background: var(--color-paper-2); 189 border: var(--rule-hair) solid var(--color-rule); 190 border-radius: var(--radius-md); 191 padding: var(--space-lg); 192 margin: 0; 193 overflow-x: auto; 194 color: var(--color-ink); 195} 196 197.session .c { color: var(--color-neutral); } 198.session .p { color: var(--color-accent); } 199.session .a { color: var(--color-info); } 200.session .ok { color: var(--color-ok); } 201.session .dim { color: var(--color-neutral); } 202 203.panel { 204 padding-block: var(--space-xl); 205 border-top: var(--rule-hair) solid var(--color-rule); 206} 207 208.panel__title { 209 font-size: var(--text-lg); 210 margin: 0 0 var(--space-sm); 211} 212 213.panel__body { 214 max-width: var(--measure); 215 color: var(--color-muted); 216 margin: 0 0 var(--space-lg); 217} 218 219.signin { 220 display: flex; 221 flex-wrap: wrap; 222 align-items: end; 223 gap: var(--space-sm); 224} 225 226.signin__group { 227 display: flex; 228 flex-direction: column; 229 gap: var(--space-2xs); 230} 231 232.signin__label { 233 font-family: var(--font-mono); 234 font-size: var(--text-xs); 235 letter-spacing: 0.06em; 236 text-transform: uppercase; 237 color: var(--color-muted); 238} 239 240.signin__field { 241 display: flex; 242 align-items: center; 243 background: var(--color-paper-2); 244 border: var(--rule-hair) solid var(--color-rule-interactive); 245 border-radius: var(--radius-sm); 246} 247 248.signin__field:focus-within { 249 outline: 2px solid var(--color-focus); 250 outline-offset: 2px; 251 border-color: var(--color-accent); 252} 253 254.signin__prompt { 255 padding-inline: 0.75ch 0; 256 color: var(--color-accent); 257 font-family: var(--font-mono); 258} 259 260.signin input:focus-visible { 261 outline: none; 262} 263 264.signin input { 265 flex: 1; 266 min-width: 14rem; 267 padding: 0.55rem 0.75rem 0.55rem 0.4ch; 268 border: 0; 269 background: transparent; 270 color: var(--color-ink); 271 font-family: var(--font-mono); 272 font-size: var(--text-base); 273} 274 275.signin input::placeholder { color: var(--color-neutral); } 276 277.btn { 278 display: inline-block; 279 padding: 0.55rem 1.1rem; 280 border: var(--rule-hair) solid var(--color-rule-interactive); 281 border-radius: var(--radius-sm); 282 background: var(--color-paper-2); 283 color: var(--color-ink); 284 font-family: var(--font-mono); 285 font-size: var(--text-sm); 286 text-decoration: none; 287 white-space: nowrap; 288 cursor: pointer; 289 transition: transform var(--dur-micro) var(--ease-out), border-color var(--dur-micro) var(--ease-out); 290} 291 292.btn:hover { border-color: var(--color-accent); transform: translateY(-1px); color: var(--color-ink); } 293.btn:active { transform: translateY(0); } 294 295.btn--primary { 296 border-color: var(--color-accent); 297 color: var(--color-accent); 298} 299 300.foot-stmt { 301 display: grid; 302 gap: var(--space-lg); 303 padding-block: var(--space-2xl) var(--space-xl); 304 border-top: var(--rule-hair) solid var(--color-rule); 305} 306 307.foot-stmt__line { 308 font-family: var(--font-display); 309 font-size: clamp(1.5rem, 4vw, 2.4rem); 310 line-height: 1.05; 311 letter-spacing: -0.02em; 312 max-width: 22ch; 313 margin: 0; 314} 315 316.foot-stmt__meta { 317 display: flex; 318 justify-content: space-between; 319 align-items: center; 320 flex-wrap: wrap; 321 gap: var(--space-sm); 322 padding-top: var(--space-sm); 323 border-top: var(--rule-hair) solid var(--color-rule); 324 font-size: var(--text-sm); 325} 326 327.muted { color: var(--color-neutral); } 328.muted a { color: var(--color-muted); } 329</style>