idk
22 unmodified lines23242526272822 unmodified lines*.sln*.sw?/tmp/result.envrc.direnv888 unmodified linesExpand all8898908918928938938948958968978988999009018958969029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499009019509519529539549559569579589599049059609619629639649089099659669679689699129139709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102591791810261027102810291030103110329219229239249259261033103410351036103710389291039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109493110951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912309351231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282940941128312841285128612871288944128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013119461312948131313141315131695195295395413171318131913201321132213231324132595795813261327132813291330133113329619621333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390966967968969139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567665 unmodified linesExpand all223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256577 unmodified linesExpand all283428352836283722342235283828392840284128427 unmodified lines888 unmodified linesExpand all height: 100%; min-width: 1px;}
.untangled-home-grid {.untangled-home-page { display: flex; min-width: 0; flex-direction: column; gap: 1rem;}
.untangled-home-command { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 1.25rem; grid-template-columns: minmax(0, 1fr) minmax(22rem, 32rem); align-items: center; gap: 1rem; border-bottom: 1px solid rgb(229 231 235); padding: 0.5rem 0.25rem 1rem;}
.untangled-home-brand { display: flex; min-width: 0; align-items: center; gap: 0.75rem;}
.untangled-home-brand h1 { margin: 0; font-size: 1.875rem; font-weight: 800; letter-spacing: 0; line-height: 1.1;}
.untangled-home-brand p { margin: 0.125rem 0 0; color: rgb(107 114 128); font-size: 0.875rem;}
.untangled-home-jump { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 0.5rem;}
.untangled-home-jump .untangled-input { min-height: 2.25rem; padding-top: 0.375rem; padding-bottom: 0.375rem;}
.untangled-home-workspace { display: grid; grid-template-columns: 14.5rem minmax(0, 1fr) 16rem; gap: 1rem; align-items: start;}
.untangled-home-grid > :first-child { padding: 1.5rem;.untangled-home-side { position: sticky; top: 0.75rem; display: flex; min-width: 0; flex-direction: column; gap: 0.75rem; padding: 0.75rem 0;}
.untangled-home-grid > :first-child > .flex:first-child { margin-bottom: 1.5rem;.untangled-home-side-left { border-right: 1px solid rgb(229 231 235); padding-right: 1rem;}
.untangled-home-grid form .flex { align-items: stretch;.untangled-home-side-right { border-left: 1px solid rgb(229 231 235); padding-left: 1rem;}
.untangled-home-grid form button { min-width: 4.5rem;.untangled-home-section-header { display: flex; align-items: center; gap: 0.5rem; padding-bottom: 0.5rem; border-bottom: 1px solid rgb(229 231 235); color: inherit;}
.untangled-home-section-header h2 { margin: 0; font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: rgb(107 114 128);}
.untangled-home-feed { min-width: 0;}
.untangled-home-feed-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid rgb(229 231 235); margin-bottom: 0.5rem;}
.untangled-home-feed-title { margin: 0; display: flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: rgb(107 114 128);}
.untangled-home-live-pill { display: inline-flex; align-items: center; justify-content: center; border: 1px solid rgb(209 213 219); border-radius: 9999px; padding: 0.25rem 0.625rem; color: rgb(75 85 99); font-size: 0.75rem; white-space: nowrap;}
.untangled-home-grid form button:hover { color: rgb(255 255 255);.untangled-home-activity-list { display: flex; min-width: 0; flex-direction: column; gap: 1rem;}
.untangled-home-grid a { transition: background-color 150ms, border-color 150ms, color 150ms, transform 150ms;/* upstream-style: individual bordered card per event, with connector lines */.untangled-home-activity-card-wrap { position: relative; min-width: 0;}
.untangled-home-grid a:hover {.untangled-home-activity-connector { position: absolute; left: 1rem; top: -1rem; width: 1px; height: 1rem; background: rgb(209 213 219);}
.untangled-home-activity-card { display: flex; flex-direction: column; border: 1px solid rgb(229 231 235); border-radius: 0.125rem; overflow: hidden;}
.untangled-home-activity-card-header { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; color: rgb(75 85 99); font-size: 0.8125rem;}
.untangled-home-activity-header-text { display: flex; min-width: 0; flex: 1; flex-wrap: wrap; align-items: center; gap: 0.375rem; overflow: hidden;}
.untangled-home-activity-repo-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: rgb(17 24 39); font-size: 0.8125rem;}
.untangled-home-activity-time { flex-shrink: 0; color: rgb(107 114 128); font-size: 0.75rem;}
.untangled-home-activity-card-body { display: block; padding: 0.5rem 0.75rem; background: rgb(249 250 251); border-color: rgb(156 163 175); border-top: 1px solid rgb(229 231 235); color: rgb(55 65 81); font-size: 0.8125rem; line-height: 1.4; text-decoration: none; transition: background-color 150ms; -webkit-box-orient: vertical; -webkit-line-clamp: 2; display: -webkit-box; overflow: hidden;}
.untangled-home-activity-card-body:hover { background: rgb(243 244 246); text-decoration: none;}
.untangled-home-activity-icon { display: inline-flex; width: 1.5rem; height: 1.5rem; align-items: center; justify-content: center; border-radius: 9999px; background: rgb(239 246 255); color: rgb(37 99 235); flex-shrink: 0;}
.untangled-home-activity-icon-issue { background: rgb(220 252 231); color: rgb(22 101 52);}
.untangled-home-activity-icon-pull { background: rgb(243 232 255); color: rgb(126 34 206);}
.untangled-home-activity-icon-repo { background: rgb(241 245 249); color: rgb(51 65 85);}
/* old copy/meta styles no longer used but kept for compat with following activity */.untangled-home-activity-copy { display: flex; min-width: 0; flex-direction: column; gap: 0.375rem;}
.untangled-home-activity-title { display: flex; min-width: 0; flex-wrap: wrap; align-items: baseline; gap: 0.35rem; color: rgb(17 24 39); font-size: 0.9375rem;}
.untangled-home-activity-title strong { min-width: 0; overflow-wrap: anywhere; font-weight: 700;}
.untangled-home-activity-body { display: -webkit-box; overflow: hidden; color: rgb(75 85 99); font-size: 0.875rem; line-height: 1.4; -webkit-box-orient: vertical; -webkit-line-clamp: 2;}
.untangled-home-activity-meta { display: flex; align-items: center; gap: 0.5rem; color: rgb(107 114 128); font-size: 0.8125rem;}
.untangled-home-feed-empty { display: flex; min-height: 18rem; flex-direction: column; align-items: center; justify-content: center; gap: 0.5rem; padding: 2rem; text-align: center; color: rgb(107 114 128);}
.untangled-home-feed-empty h3 { margin: 0.5rem 0 0; color: rgb(31 41 55); font-size: 1rem; font-weight: 700;}
.untangled-home-feed-empty p { max-width: 22rem; margin: 0; font-size: 0.875rem; line-height: 1.5;}
.untangled-home-compact-list,.untangled-home-following-list { display: flex; min-width: 0; flex-direction: column; gap: 0;}
/* upstream repoCard style: flat link with icon + name, description below, time at right */.untangled-home-repo-row { display: flex; min-width: 0; flex-direction: column; gap: 0.125rem; border: 1px solid rgb(229 231 235); border-radius: 0.125rem; padding: 0.625rem 0.75rem; margin-bottom: 0.25rem; color: inherit; text-decoration: none; transition: background-color 150ms; background: rgb(255 255 255);}
.untangled-home-grid > :first-child a {.untangled-home-repo-row:last-child { border-bottom-width: 1px; margin-bottom: 0;}
.untangled-home-repo-row:hover { background: rgb(249 250 251); text-decoration: none;}
.untangled-home-repo-row-name { display: flex; align-items: center; gap: 0.375rem; min-width: 0; font-weight: 600; font-size: 0.875rem; color: rgb(17 24 39); overflow: hidden;}
.untangled-home-repo-row-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
.untangled-home-repo-row-desc { color: rgb(75 85 99); font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;}
.untangled-home-repo-row-time { color: rgb(107 114 128); font-size: 0.75rem; margin-top: 0.125rem;}
.untangled-home-following-row { display: flex; min-width: 0; align-items: center; gap: 0.5rem; padding: 0.375rem 0.125rem; color: inherit; transition: background-color 150ms;}
.untangled-home-grid > .flex.flex-col.gap-4 { min-height: 0;.untangled-home-following-row span { display: flex; min-width: 0; flex-direction: column;}
.untangled-home-grid > .flex.flex-col.gap-4 > .rounded {.untangled-home-following-row span span { overflow: hidden; color: rgb(17 24 39); font-size: 0.8125rem; font-weight: 600; text-overflow: ellipsis; white-space: nowrap;}
.untangled-home-following-row small { color: rgb(107 114 128); font-size: 0.75rem;}
.untangled-home-empty { padding: 0.75rem 0.125rem; color: rgb(107 114 128); font-size: 0.875rem; line-height: 1.4;}
.untangled-home-below { display: flex; min-height: 0; flex-direction: column; padding: 1.25rem; gap: 1.5rem; padding-top: 0.5rem;}
.untangled-home-grid > .flex.flex-col.gap-4 > .rounded > .flex.flex-col.gap-2 { max-height: 16rem; overflow-y: auto; padding-right: 0.125rem;.untangled-home-wide-section { display: flex; flex-direction: column; gap: 0.75rem; min-width: 0; border-top: 1px solid rgb(209 213 219); padding-top: 0.75rem;}
.untangled-home-grid > .flex.flex-col.gap-4 > .rounded a { display: block;.untangled-home-lower-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(18rem, 0.8fr); gap: 1.5rem; align-items: start;}
.untangled-home-grid > .flex.flex-col.gap-4 > .rounded a:hover { background: rgb(249 250 251);.untangled-home-recent-strip,.untangled-home-sample-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; margin-top: 0.75rem; border-top: 1px solid rgb(229 231 235); border-left: 1px solid rgb(229 231 235);}
.untangled-home-recent-link,.untangled-home-sample-link { display: flex; min-width: 0; flex-direction: column; gap: 0.25rem; border-right: 1px solid rgb(229 231 235); border-bottom: 1px solid rgb(229 231 235); padding: 0.875rem; color: inherit; text-decoration: none; transition: background-color 150ms;}
.untangled-home-recent-link:hover,.untangled-home-sample-link:hover { background: rgb(255 255 255); text-decoration: none;}
.untangled-home-recent-link span,.untangled-home-sample-link strong { overflow: hidden; color: rgb(17 24 39); font-size: 0.875rem; font-weight: 700; text-overflow: ellipsis; white-space: nowrap;}
.untangled-home-recent-link small,.untangled-home-sample-link span { overflow: hidden; color: rgb(107 114 128); font-size: 0.75rem; text-overflow: ellipsis; white-space: nowrap;}
.untangled-home-sample-list { grid-template-columns: repeat(3, minmax(0, 1fr));}
.untangled-home-sample-list-compact { grid-template-columns: minmax(0, 1fr);}
@media (prefers-color-scheme: dark) { .untangled-home-grid a:hover, .untangled-home-grid > .flex.flex-col.gap-4 > .rounded a:hover { background: rgb(55 65 81 / 0.4); border-color: rgb(75 85 99); .untangled-home-command, .untangled-home-side-left, .untangled-home-side-right, .untangled-home-section-header, .untangled-home-feed-header, .untangled-home-wide-section, .untangled-home-recent-strip, .untangled-home-sample-list, .untangled-home-recent-link, .untangled-home-sample-link { border-color: rgb(55 65 81); }
.untangled-home-activity-card, .untangled-home-repo-row { border-color: rgb(55 65 81); }
.untangled-home-activity-connector { background: rgb(55 65 81); }
.untangled-home-activity-card-header { background: rgb(31 41 55); color: rgb(156 163 175); }
.untangled-home-activity-card-body { background: rgb(17 24 39); border-color: rgb(55 65 81); color: rgb(209 213 219); }
.untangled-home-activity-card-body:hover { background: rgb(31 41 55); }
.untangled-home-activity-repo-label { color: rgb(243 244 246); }
.untangled-home-activity-time { color: rgb(107 114 128); }
.untangled-home-repo-row { background: rgb(31 41 55); }
.untangled-home-repo-row:hover { background: rgb(55 65 81 / 0.6); }
.untangled-home-repo-row-name { color: rgb(243 244 246); }
.untangled-home-repo-row-desc, .untangled-home-repo-row-time, .untangled-home-following-row small { color: rgb(107 114 128); }
.untangled-home-brand p, .untangled-home-activity-body, .untangled-home-activity-meta, .untangled-home-feed-empty, .untangled-home-empty, .untangled-home-recent-link small, .untangled-home-sample-link span { color: rgb(156 163 175); }
.untangled-home-feed-empty h3, .untangled-home-activity-title, .untangled-home-following-row span span, .untangled-home-recent-link span, .untangled-home-sample-link strong { color: rgb(243 244 246); }
.untangled-home-live-pill { border-color: rgb(75 85 99); color: rgb(209 213 219); }
.untangled-home-recent-link:hover, .untangled-home-sample-link:hover { background: rgb(55 65 81 / 0.35); }
.untangled-home-activity-icon { background: rgb(15 23 42); color: rgb(191 219 254); }
.untangled-home-activity-icon-issue { background: rgb(22 101 52 / 0.45); color: rgb(134 239 172); }
.untangled-home-activity-icon-pull { background: rgb(88 28 135 / 0.55); color: rgb(216 180 254); }
.untangled-home-activity-icon-repo { background: rgb(15 23 42); color: rgb(191 219 254); }}
@media (max-width: 1023px) { .untangled-home-command { grid-template-columns: minmax(0, 1fr); }
.untangled-home-workspace { display: flex; flex-direction: column; }
.untangled-home-feed { order: -1; width: 100%; }
.untangled-home-side { position: static; width: 100%; border-right: 0; border-left: 0; border-top: 1px solid rgb(229 231 235); padding: 0.75rem 0 0; }
.untangled-home-lower-grid, .untangled-home-recent-strip, .untangled-home-sample-list { grid-template-columns: minmax(0, 1fr); }}
@media (max-width: 1023px) and (prefers-color-scheme: dark) { .untangled-home-side { border-top-color: rgb(55 65 81); }}
@media (max-width: 767px) { .untangled-home-brand h1 { font-size: 1.5rem; }
.untangled-home-jump { grid-template-columns: auto minmax(0, 1fr); }
.untangled-home-jump button { grid-column: 1 / -1; }
.untangled-home-feed-header { align-items: flex-start; flex-direction: column; }
.untangled-home-activity-row { padding-right: 1rem; padding-left: 1rem; }}
.untangled-safe-footer { padding-bottom: env(safe-area-inset-bottom);}
665 unmodified linesExpand all}
.untangled-code-line-number:hover { text-decoration: underline; cursor: pointer;}
/* Highlighted line (selected via line number click or URL hash) */.untangled-code-line--hl .untangled-code-line-number,.untangled-code-line--hl .untangled-code-line-content { background-color: rgb(254 240 138 / 0.4);}
@media (prefers-color-scheme: dark) { .untangled-code-line--hl .untangled-code-line-number, .untangled-code-line--hl .untangled-code-line-content { background-color: rgb(161 98 7 / 0.3); }}
.untangled-code-line-content { padding-right: 1.5rem; color: var(--untangled-code-fg);}577 unmodified linesExpand all}
@media (max-width: 767px) { .untangled-repo-overview, .untangled-filter-grid, .untangled-home-grid { .untangled-filter-grid { grid-template-columns: minmax(0, 1fr); }
.untangled-repo-filter-search {7 unmodified lines1015 unmodified linesExpand all10161017101810191020102110221023102410251026102710281029103010311032103316 unmodified lines105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111051105210531054105510561057105811121113111411151116111711181119112011211122112311241125112611271128112911301131472 unmodified linesExpand all1015 unmodified linesExpand all const highlighted = highlightCodeLines(content, language)[0] ?? escapeHtml(content); return `<span class="untangled-code-diff-op">${escapeHtml(marker)}</span>${highlighted}`;};
/** Parse a URL hash like #L5 or #L3-L8 into {start, end} (1-based, inclusive). */const parseLineHash = (hash: string): { start: number; end: number } | null => { const m = /^#L(\d+)(?:-L?(\d+))?$/.exec(hash); if (!m) return null; const start = parseInt(m[1], 10); const end = m[2] ? parseInt(m[2], 10) : start; if (isNaN(start) || isNaN(end)) return null; return { start: Math.min(start, end), end: Math.max(start, end) };};
export const CodeView: Component<{ text: string; id?: string; wrap?: boolean;16 unmodified lines line.startsWith('@@') && 'untangled-code-line-hunk', ) : undefined;
// Line selection state (only used in non-diff mode) const [selection, setSelection] = createSignal<{ start: number; end: number } | null>(null); // The line number of the last plain click (anchor for shift-click ranges) let anchorLine: number | null = null;
const applyHashSelection = (scroll = false) => { const parsed = parseLineHash(window.location.hash); setSelection(parsed); if (scroll && parsed) { requestAnimationFrame(() => { const target = document.getElementById(`L${parsed.start}`); target?.scrollIntoView({ behavior: 'instant', block: 'center' }); }); } };
const handleLineNumberClick = (lineNum: number, event: MouseEvent) => { event.preventDefault(); let hash: string; if (event.shiftKey && anchorLine !== null) { const lo = Math.min(anchorLine, lineNum); const hi = Math.max(anchorLine, lineNum); hash = `#L${lo}-L${hi}`; } else { anchorLine = lineNum; hash = `#L${lineNum}`; } history.pushState(null, '', hash); setSelection(parseLineHash(hash)); };
onMount(() => { if (props.diff) return;
applyHashSelection(true);
const onHashChange = () => applyHashSelection(false); window.addEventListener('hashchange', onHashChange); window.addEventListener('popstate', onHashChange); onCleanup(() => { window.removeEventListener('hashchange', onHashChange); window.removeEventListener('popstate', onHashChange); }); });
const isLineHighlighted = (lineNum: number) => { const s = selection(); if (!s) return false; return lineNum >= s.start && lineNum <= s.end; };
return ( <div class="untangled-code-scroll" style={{ 'max-height': props.maxHeight }}> <div id={props.id} class={clsx('untangled-code-view', props.wrap && 'untangled-code-view-wrap')} > <For each={lines()}> {(line, index) => ( <div class={clsx('untangled-code-line', lineClass(line))}> <a id={`L${index() + 1}`} href={`#L${index() + 1}`} class="untangled-code-line-number"> {index() + 1} </a> <span class="untangled-code-line-content" innerHTML={highlightedLines()[index()] || ' '} /> </div> )} {(line, index) => { const lineNum = index() + 1; return ( <div class={clsx('untangled-code-line', lineClass(line), !props.diff && isLineHighlighted(lineNum) && 'untangled-code-line--hl')}> <a id={`L${lineNum}`} href={`#L${lineNum}`} class="untangled-code-line-number" onClick={props.diff ? undefined : (e) => handleLineNumberClick(lineNum, e)} > {lineNum} </a> <span class="untangled-code-line-content" innerHTML={highlightedLines()[index()] || ' '} /> </div> ); }} </For> </div> </div> );472 unmodified linesExpand all1234567891011122131441516617181982021221023242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611622016316422165166167168169170291711721731741753334353617617717817918018118218318418518618718818919019119219319419519619719819920020120220340412042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582594445464748495051522602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113125556575859606162636465666768313314315316317318319320321322323324325326327328329330331332333334335336337338339707172737475767778798081828384853403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763778788893783793803813829238338494385386387979899388389390391392393394395396397107108109398399400401111112402403404114115116117118119120121122123124125126127128405406407408409410411412413414415416417418419420421422423424425130131132426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537import clsx from 'clsx';import { BookMarked, CircleDot, FolderCode, GalleryVertical, GitPullRequest, LoaderCircle, Star, Users, UserRound,} from 'lucide-solid';import { A, useNavigate } from '@solidjs/router';import { A } from '@solidjs/router';import { createQuery } from '@tanstack/solid-query';import { For, Show, createMemo, createSignal, type Component } from 'solid-js';import { For, Show, createEffect, createMemo, onCleanup, type Component, type JSX } from 'solid-js';
import { LoadingState, buttonStyles, cardStyles, inputStyles } from '../components/common';import { Avatar, ErrorState, LoadingState } from '../components/common';import { listFollowRecords } from '../lib/api/graph';import { resolveActor } from '../lib/api/identity';import { listRepoRecords } from '../lib/api/repos';import { listRepoRecords, type RepoRecord } from '../lib/api/repos';import { useAuth } from '../lib/auth';import { useLiveEvents, type LiveEvent } from '../lib/live-events';import { formatRelativeTime, loadRecentRepos, parseRepoJump } from '../lib/repo-utils';import { formatRelativeTime, loadRecentRepos } from '../lib/repo-utils';
const SAMPLE_REPOS = [ { owner: 'mary.my.id', slug: 'atcute' }, { owner: 'mary.my.id', slug: 'boat' }, { owner: 'mary.my.id', slug: 'aglais' },];
const HOME_ACTIVITY_SOURCES = [ 'sh.tangled.repo.issue:repoDid', 'sh.tangled.repo.issue:repo', 'sh.tangled.repo.pull:target.repo',];
type HomeRepoItem = { owner: string; slug: string; title: string; href: string; rkey: string; repoDid?: string; description?: string; createdAt?: string;};
type HomeActivityKind = 'issue' | 'pull' | 'repo';
type HomeActivityItem = { id: string; kind: HomeActivityKind; title: string; repoLabel: string; href: string; at?: string; body?: string; actorDid?: string;};
type FollowingHomeData = { actors: Array<{ did: string; handle: string; repoCount: number; }>; activity: HomeActivityItem[];};
const repoSlug = (repo: RepoRecord): string => repo.value.name?.trim() || repo.rkey;
const repoTimestamp = (repo: HomeRepoItem): number => { const timestamp = repo.createdAt ? new Date(repo.createdAt).getTime() : 0; return Number.isFinite(timestamp) ? timestamp : 0;};
const activityTimestamp = (item: HomeActivityItem): number => { const timestamp = item.at ? new Date(item.at).getTime() : 0; return Number.isFinite(timestamp) ? timestamp : 0;};
const formatCount = (count: number, singular: string, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
const homeRepoItem = (owner: string, repo: RepoRecord): HomeRepoItem => { const slug = repoSlug(repo); return { owner, slug, title: `${owner}/${slug}`, href: `/${owner}/${slug}`, rkey: repo.rkey, repoDid: repo.value.repoDid, description: repo.value.description, createdAt: repo.value.createdAt, };};
const activityLabelForEvent = (event: LiveEvent): Pick<HomeActivityItem, 'kind' | 'title'> => { if (event.source.includes('.pull')) { return { kind: 'pull', title: 'new pull request' }; }
return { kind: 'issue', title: 'new issue' };};
const eventActivityHref = (repo: HomeRepoItem, event: LiveEvent): string => { const collection = event.source.includes('.pull') ? 'pulls' : 'issues'; return `${repo.href}/${collection}/${encodeURIComponent(event.sourceRecord)}`;};
const fetchFollowingHomeData = async (viewerDid: string): Promise<FollowingHomeData> => { const follows = await listFollowRecords(viewerDid); const resolved = await Promise.all( follows.slice(0, 8).map(async (follow) => { try { const actor = await resolveActor(follow.value.subject); const repos = await listRepoRecords(actor).catch(() => []); return { follow, actor, repos, }; } catch { return null; } }), );
const actors = resolved .filter((item): item is NonNullable<typeof item> => Boolean(item)) .map((item) => ({ did: item.actor.did, handle: item.actor.handle, repoCount: item.repos.length, }));
const activity = resolved .filter((item): item is NonNullable<typeof item> => Boolean(item)) .flatMap((item) => item.repos.map((repo) => { const repoItem = homeRepoItem(item.actor.handle, repo); return { id: `following-repo:${repo.uri}`, kind: 'repo' as const, title: 'repository published', repoLabel: repoItem.title, href: repoItem.href, at: repoItem.createdAt, body: repoItem.description, actorDid: item.actor.did, }; }), ) .sort((left, right) => activityTimestamp(right) - activityTimestamp(left)) .slice(0, 8);
return { actors, activity };};
export const HomePage: Component = () => { const auth = useAuth(); const [jump, setJump] = createSignal(''); const live = useLiveEvents(); const recentRepos = createMemo(loadRecentRepos); const navigate = useNavigate();
const ownReposQuery = createQuery(() => ({ queryKey: ['own-repos', auth.currentDid()], enabled: Boolean(auth.currentDid()), queryFn: async () => { const actor = await resolveActor(auth.currentDid()!); return listRepoRecords(actor); const records = await listRepoRecords(actor); return { actor, records }; }, }));
const onSubmit = (event: SubmitEvent) => { event.preventDefault(); const parsed = parseRepoJump(jump()); if (!parsed) { const followingQuery = createQuery(() => ({ queryKey: ['home-following', auth.currentDid()], enabled: Boolean(auth.currentDid()), queryFn: async () => fetchFollowingHomeData(auth.currentDid()!), }));
const ownRepoItems = createMemo(() => { const data = ownReposQuery.data; if (!data) return []; return data.records .map((repo) => homeRepoItem(data.actor.handle, repo)) .sort((left, right) => repoTimestamp(right) - repoTimestamp(left)); });
const repoByDid = createMemo(() => { const repos = new Map<string, HomeRepoItem>(); for (const repo of ownRepoItems()) { if (repo.repoDid) repos.set(repo.repoDid, repo); } return repos; });
createEffect(() => { if (!auth.currentDid()) { live.clear(); return; }
navigate(`/${parsed.owner}/${parsed.repo}`); }; if (ownReposQuery.isLoading) { return; }
const subjects = ownRepoItems() .map((repo) => repo.repoDid) .filter((repoDid): repoDid is string => Boolean(repoDid));
if (subjects.length === 0) { live.clear(); return; }
live.subscribe({ subjects, sources: HOME_ACTIVITY_SOURCES, });
onCleanup(() => { live.clear(); }); });
const activityItems = createMemo<HomeActivityItem[]>(() => { const repos = repoByDid(); const liveItems = live.events() .map((event): HomeActivityItem | null => { const repo = repos.get(event.subject); if (!repo) return null; const label = activityLabelForEvent(event); return { id: `live:${event.sourceRecord}:${event.receivedAt}`, ...label, repoLabel: repo.title, href: eventActivityHref(repo, event), at: event.receivedAt, }; }) .filter((item): item is HomeActivityItem => Boolean(item));
const repoItems: HomeActivityItem[] = ownRepoItems().map((repo) => ({ id: `repo:${repo.owner}/${repo.rkey}`, kind: 'repo' as const, title: 'repository published', repoLabel: repo.title, href: repo.href, at: repo.createdAt, body: repo.description, }));
return [...liveItems, ...repoItems] .sort((left, right) => activityTimestamp(right) - activityTimestamp(left)) .slice(0, 8); });
return ( <div class="untangled-home-grid"> <div class="flex flex-col gap-4"> <div class={cardStyles('p-6')}> <div class="flex items-center gap-3 mb-6"> <img src="/static/logos/dolly.svg" alt="" class="size-12" /> <div> <h1 class="text-2xl font-bold m-0">untangled</h1> <p class="m-0 text-sm text-gray-500 dark:text-gray-400">tightly-knit social coding</p> </div> <div class="untangled-home-page"> <section class="untangled-home-workspace"> <aside class="untangled-home-side untangled-home-side-left"> <HomeSectionHeader icon={<FolderCode class="size-4" />} title="your repositories" /> <Show when={auth.currentDid()} fallback={<HomeEmpty label="Sign in to see your repositories." />} > <Show when={!ownReposQuery.isLoading} fallback={<LoadingState label="Loading repositories..." />} > <Show when={!ownReposQuery.error} fallback={<ErrorState message={ownReposQuery.error instanceof Error ? ownReposQuery.error.message : 'Failed to load repositories'} />} > <Show when={ownRepoItems().length > 0} fallback={<HomeEmpty label="No repositories yet." />} > <div class="untangled-home-compact-list"> <For each={ownRepoItems().slice(0, 6)}> {(repo) => ( <RepoLinkRow href={repo.href} title={repo.slug} description={repo.description} meta={repo.createdAt ? formatRelativeTime(repo.createdAt) : undefined} /> )} </For> </div> </Show> </Show> </Show> </Show> </aside>
<main class="untangled-home-feed"> <div class="untangled-home-feed-header"> <h2 class="untangled-home-feed-title"> <GalleryVertical class="size-5" /> activity </h2> <Show when={auth.currentDid()}> <span class="untangled-home-live-pill"> <Show when={ownRepoItems().length > 0} fallback="idle"> {formatCount(ownRepoItems().length, 'repo')} watched </Show> </span> </Show> </div>
<form onSubmit={onSubmit} class="flex flex-col gap-3"> <label class="text-sm font-medium">repository</label> <div class="flex gap-2"> <input value={jump()} onInput={(event) => setJump(event.currentTarget.value)} placeholder="mary.my.id/atcute" class={inputStyles()} /> <button type="submit" class={clsx(buttonStyles('primary'), 'shrink-0')}> open </button> </div> </form> <Show when={auth.currentDid()} fallback={<HomeFeedEmpty title="Sign in for repo activity" body="Issues, pull requests, and repositories from your account will appear here." />} > <Show when={!ownReposQuery.isLoading} fallback={<HomeFeedLoading />} > <Show when={!ownReposQuery.error} fallback={<ErrorState message={ownReposQuery.error instanceof Error ? ownReposQuery.error.message : 'Failed to load activity'} />} > <Show when={activityItems().length > 0} fallback={<HomeFeedEmpty title="No repo activity yet" body="New repository, issue, and pull request activity will land here." />} > <div class="untangled-home-activity-list"> <For each={activityItems()}> {(item, index) => <ActivityRow item={item} index={index()} />} </For> </div> </Show> </Show> </Show> </Show> </main>
<div class="mt-8"> <div class="text-sm font-medium mb-3">sample repositories</div> <div class="grid gap-2 sm:grid-cols-2"> <For each={SAMPLE_REPOS}> {(repo) => ( <A href={`/${repo.owner}/${repo.slug}`} class={cardStyles('p-4 hover:bg-gray-50 dark:hover:bg-gray-700/30 no-underline')}> <div class="font-medium"> {repo.owner}/{repo.slug} </div> </A> )} </For> </div> </div> </div> </div> <aside class="untangled-home-side untangled-home-side-right"> <HomeSectionHeader icon={<Users class="size-4" />} title="following" /> <Show when={auth.currentDid()} fallback={<HomeEmpty label="Sign in to see followed developers." />} > <Show when={!followingQuery.isLoading} fallback={<LoadingState label="Loading following..." />} > <Show when={!followingQuery.error} fallback={<ErrorState message={followingQuery.error instanceof Error ? followingQuery.error.message : 'Failed to load following'} />} > <Show when={(followingQuery.data?.actors.length ?? 0) > 0} fallback={<HomeEmpty label="Not following anyone yet." />} > <div class="untangled-home-following-list"> <For each={followingQuery.data?.actors.slice(0, 5) ?? []}> {(actor) => ( <div class="untangled-home-following-row"> <Avatar did={actor.did} size="size-6" /> <span class="min-w-0"> <span>{actor.handle}</span> <small>{formatCount(actor.repoCount, 'repo')}</small> </span> </div> )} </For> </div> </Show> </Show> </Show> </Show> </aside> </section>
<div class="flex flex-col gap-4"> <div class={cardStyles('p-6')}> <div class="text-sm font-medium mb-3">recent</div> <section class="untangled-home-below"> <div class="untangled-home-wide-section"> <HomeSectionHeader icon={<Star class="size-4" />} title="recent repositories" /> <Show when={recentRepos().length > 0} fallback={<div class="text-sm text-gray-500 dark:text-gray-400">No recent repositories.</div>} fallback={<HomeEmpty label="No recent repositories." />} > <div class="flex flex-col gap-2"> <div class="untangled-home-recent-strip"> <For each={recentRepos()}> {(repo) => ( <A href={`/${repo.owner}/${repo.slug}`} class="no-underline hover:no-underline rounded border border-gray-200 dark:border-gray-700 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/30"> <div class="font-medium">{repo.title}</div> <div class="text-xs text-gray-500 dark:text-gray-400">{formatRelativeTime(repo.updatedAt)}</div> <A href={`/${repo.owner}/${repo.slug}`} class="untangled-home-recent-link"> <span>{repo.title}</span> <small>{formatRelativeTime(repo.updatedAt)}</small> </A> )} </For> </div> </Show> </div>
<Show when={auth.currentDid()}> <div class={cardStyles('p-6')}> <div class="text-sm font-medium mb-3">your repositories</div> <div class="untangled-home-lower-grid"> <section class="untangled-home-wide-section"> <HomeSectionHeader icon={<UserRound class="size-4" />} title="following activity" /> <Show when={!ownReposQuery.isLoading} fallback={<LoadingState label="Loading repositories..." />} when={auth.currentDid()} fallback={<HomeEmpty label="Sign in to see followed repository activity." />} > <div class="flex flex-col gap-2"> <For each={ownReposQuery.data ?? []}> {(repo) => ( <A href={`/${auth.currentDid()}/${repo.value.name?.trim() || repo.rkey}`} class="no-underline hover:no-underline rounded border border-gray-200 dark:border-gray-700 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/30" > <div class="font-medium">{repo.value.name?.trim() || repo.rkey}</div> <div class="text-xs text-gray-500 dark:text-gray-400"> {repo.value.description || repo.value.repoDid} </div> </A> )} </For> </div> <Show when={!followingQuery.isLoading} fallback={<LoadingState label="Loading followed repositories..." />} > <Show when={!followingQuery.error} fallback={<ErrorState message={followingQuery.error instanceof Error ? followingQuery.error.message : 'Failed to load following activity'} />} > <Show when={(followingQuery.data?.activity.length ?? 0) > 0} fallback={<HomeEmpty label="No followed repository activity yet." />} > <div class="untangled-home-activity-list"> <For each={followingQuery.data?.activity ?? []}> {(item, index) => <ActivityRow item={item} index={index()} />} </For> </div> </Show> </Show> </Show> </Show> </div> </Show> </div> </section>
<section class="untangled-home-wide-section"> <HomeSectionHeader icon={<FolderCode class="size-4" />} title="sample repositories" /> <HomeSampleRepos compact /> </section> </div> </section> </div> );};
const HomeSectionHeader: Component<{ icon: JSX.Element; title: string }> = (props) => ( <header class="untangled-home-section-header"> {props.icon} <h2>{props.title}</h2> </header>);
const HomeEmpty: Component<{ label: string }> = (props) => ( <div class="untangled-home-empty">{props.label}</div>);
const HomeFeedEmpty: Component<{ title: string; body: string }> = (props) => ( <div class="untangled-home-feed-empty"> <FolderCode class="size-8" /> <h3>{props.title}</h3> <p>{props.body}</p> </div>);
const HomeFeedLoading: Component = () => ( <div class="untangled-home-feed-empty"> <LoaderCircle class="size-8 animate-spin" /> <h3>Loading activity</h3> <p>Checking your repositories.</p> </div>);
const RepoLinkRow: Component<{ href: string; title: string; description?: string; meta?: string;}> = (props) => ( <A href={props.href} class="untangled-home-repo-row"> <span class="untangled-home-repo-row-name"> <BookMarked class="size-3.5 flex-shrink-0" /> <span>{props.title}</span> </span> <Show when={props.description}> <small class="untangled-home-repo-row-desc">{props.description}</small> </Show> <Show when={props.meta}> <time class="untangled-home-repo-row-time">{props.meta}</time> </Show> </A>);
const ActivityRow: Component<{ item: HomeActivityItem; index: number }> = (props) => ( <div class="untangled-home-activity-card-wrap"> <Show when={props.index > 0}> <div class="untangled-home-activity-connector" /> </Show> <div class="untangled-home-activity-card"> <div class="untangled-home-activity-card-header"> <span class={clsx('untangled-home-activity-icon', `untangled-home-activity-icon-${props.item.kind}`)}> <ActivityIcon kind={props.item.kind} /> </span> <span class="untangled-home-activity-header-text"> <Show when={props.item.actorDid}> <Avatar did={props.item.actorDid!} size="size-4" /> </Show> <span>{props.item.title}</span> <strong class="untangled-home-activity-repo-label">{props.item.repoLabel}</strong> </span> <time class="untangled-home-activity-time">{formatRelativeTime(props.item.at)}</time> </div> <Show when={props.item.body}> <A href={props.item.href} class="untangled-home-activity-card-body"> {props.item.body} </A> </Show> </div> </div>);
const ActivityIcon: Component<{ kind: HomeActivityKind }> = (props) => ( <Show when={props.kind === 'pull'} fallback={ <Show when={props.kind === 'issue'} fallback={<FolderCode class="size-4" />}> <CircleDot class="size-4" /> </Show> } > <GitPullRequest class="size-4" /> </Show>);
const HomeSampleRepos: Component<{ compact?: boolean }> = (props) => ( <div class={clsx('untangled-home-sample-list', props.compact && 'untangled-home-sample-list-compact')}> <For each={SAMPLE_REPOS}> {(repo) => ( <A href={`/${repo.owner}/${repo.slug}`} class="untangled-home-sample-link"> <span>{repo.owner}</span> <strong>{repo.slug}</strong> </A> )} </For> </div>);