Something went wrong. Try again.
Website hub for the atmosphere community, aggregating posts, events, regional, and more
Something went wrong. Try again.
11 kB · 379 lines
Astro
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380---import Combobox from '../auth/Combobox.astro';import AtprotoIcon from '../auth/icons/AtprotoIcon.astro';
interface Props { shareRepo: string; shareSourceProfile: { displayName?: string; avatar?: string } | null; loggedInUserHandle: string | null;}
const { shareRepo, shareSourceProfile, loggedInUserHandle,} = Astro.props;
const canResetShareSource = !!loggedInUserHandle && shareRepo !== loggedInUserHandle;const resetSourceHref = loggedInUserHandle ? `/community-content?source=${encodeURIComponent(loggedInUserHandle)}` : '/community-content';---
<div class="share-source"> <p class="share-source__label">Sharing from</p> <details class="share-source__menu"> <summary class="share-source__summary"> <span class="share-source__chip"> {shareSourceProfile?.avatar ? ( <img class="share-source__avatar" src={shareSourceProfile.avatar} alt="" width="32" height="32" loading="lazy" decoding="async" /> ) : ( <span class="share-source__avatar share-source__avatar--fallback" aria-hidden="true"> {(shareSourceProfile?.displayName || shareRepo || '@').charAt(0).toUpperCase()} </span> )} <span class="share-source__identity"> <span class="share-source__name"> {shareSourceProfile?.displayName?.trim() || shareRepo} </span> <span class="share-source__handle">@{shareRepo}</span> </span> </span> <svg class="share-source__chevron" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true" focusable="false"> <path d="M3.5 5.25L7 8.75L10.5 5.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> </summary> <form class="share-source__form" method="GET" action="/community-content"> <Combobox id="share-source-handle" name="source" label="Content source" placeholder="handle.bsky.social" inputClass="share-source__input" autocomplete="off" inputmode="url" > <AtprotoIcon slot="leading-icon" width={18} height={18} /> </Combobox> <button class="share-source__button" type="submit">I choose them!</button> {canResetShareSource && ( <a class="share-source__link" href={resetSourceHref}> <svg class="share-source__link-icon" width="16" height="16" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" > <path fill="currentColor" fill-rule="evenodd" stroke="currentColor" stroke-linejoin="round" stroke-width="4" d="M44 40.836q-7.34-8.96-13.036-10.168t-10.846-.365V41L4 23.545L20.118 7v10.167q9.523.075 16.192 6.833q6.668 6.758 7.69 16.836Z" clip-rule="evenodd" /> </svg> <span>Go back to me</span> </a> )} </form> </details></div>
<script> import { wireDetailsPopover } from '../../lib/client/details-popover';
type Actor = { handle: string; displayName?: string; avatar?: string; }; type Suggestion = { value: string; label?: string; sublabel?: string; avatar?: string; };
const SEARCH_ACTORS_URL = 'https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead';
const searchBskyActors = async (q: string): Promise<Suggestion[]> => { const url = new URL(SEARCH_ACTORS_URL); url.searchParams.set('q', q); url.searchParams.set('limit', '8'); const res = await fetch(url, { headers: { accept: 'application/json' } }); if (!res.ok) return []; const data = (await res.json()) as { actors?: Actor[] }; return (data.actors ?? []).map((a) => ({ value: a.handle, label: a.displayName || a.handle, sublabel: `@${a.handle}`, avatar: a.avatar, })); };
const sourceInput = document.getElementById('share-source-handle') as HTMLInputElement | null; const sourceMenu = sourceInput?.closest<HTMLDetailsElement>('details.share-source__menu'); const sourceCombobox = sourceInput?.closest('cmb-combobox'); const loadSelectedSource = () => { const selectedSource = sourceInput?.value.trim(); if (!selectedSource) return;
const action = sourceInput?.form?.action ?? '/community-content'; const url = new URL(action, window.location.href); url.searchParams.set('source', selectedSource); window.location.assign(url.toString()); };
sourceCombobox?.addEventListener('combobox-search', (e) => { const detail = (e as CustomEvent<{ query: string; results: Promise<Suggestion[]> | null }>).detail; detail.results = searchBskyActors(detail.query); });
sourceCombobox?.addEventListener('autocomplete-select', () => { sourceMenu?.removeAttribute('open'); loadSelectedSource(); });
if (sourceMenu) { wireDetailsPopover(sourceMenu, { focusSelector: 'input[name="source"]', surfaceSelector: '.share-source__form', }); }</script>
<style> .share-source { display: grid; gap: var(--space-xs); max-width: min(100%, 24rem); }
.share-source__label { margin: 0; color: var(--foreground-muted); font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.02em; line-height: var(--leading-tight); }
.share-source__menu { position: relative; width: 100%; }
.share-source__summary { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); min-height: 44px; padding: var(--space-xs); border: 1px solid var(--border); border-radius: var(--radius-pill); background: linear-gradient( 145deg, color-mix(in oklch, var(--primary) 8%, var(--card)) 0%, var(--card) 60% ); color: var(--primary); list-style: none; cursor: pointer; transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; }
.share-source__summary::-webkit-details-marker { display: none; }
.share-source__summary:hover { border-color: color-mix(in oklch, var(--primary) 45%, var(--border)); background: var(--card); }
.share-source__menu[open] .share-source__summary { border-color: var(--primary); background: var(--card); box-shadow: 0 0 0 1px color-mix(in oklch, var(--primary) 20%, transparent); }
.share-source__summary:focus-visible, .share-source__button:focus-visible, :global(.share-source__input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 2px; }
.share-source__chip { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: var(--space-sm); min-width: 0; }
.share-source__avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; background: var(--primary-muted); box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--primary) 12%, transparent); }
.share-source__avatar--fallback { display: inline-flex; align-items: center; justify-content: center; color: var(--primary); font-size: var(--text-sm); font-weight: 800; }
.share-source__identity { display: grid; gap: 1px; min-width: 0; }
.share-source__name, .share-source__handle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.share-source__name { color: var(--foreground); font-size: var(--text-sm); font-weight: 800; line-height: 1.25; }
.share-source__handle { color: var(--foreground-muted); font-size: var(--text-xs); font-weight: 700; line-height: 1.25; }
.share-source__chevron { flex: 0 0 auto; margin-inline: var(--space-xs) var(--space-sm); color: var(--foreground-muted); transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1); }
.share-source__menu[open] .share-source__chevron { transform: rotate(180deg); color: var(--primary); }
.share-source__form { position: absolute; top: calc(100% + var(--space-xs)); left: 0; z-index: var(--z-popover); display: grid; gap: var(--space-sm); width: min(21rem, calc(100vw - 2 * var(--space-lg))); padding: var(--space-base); background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: var(--elevation-high); }
:global(.share-source__input) { width: 100%; min-height: 40px; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--space-sm) var(--space-md); color: var(--foreground); font: inherit; font-size: var(--text-sm); }
.share-source__button { display: inline-flex; align-items: center; justify-content: center; min-height: 36px; border: 0; border-radius: var(--radius-sm); padding: var(--space-sm) var(--space-base); background: var(--primary); color: var(--primary-foreground); font: inherit; font-size: var(--text-sm); font-weight: 700; cursor: pointer; transition: background 0.15s ease; }
.share-source__link { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-xs); min-height: 36px; border: 1px solid color-mix(in oklch, var(--foreground) 12%, var(--border)); border-radius: var(--radius-sm); padding: var(--space-sm) var(--space-base); background: color-mix(in oklch, var(--card) 84%, var(--muted)); color: var(--foreground-secondary); font-size: var(--text-sm); font-weight: 700; line-height: 1; text-decoration: none; transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; }
.share-source__link:hover { background: color-mix(in oklch, var(--card) 60%, var(--muted)); border-color: color-mix(in oklch, var(--foreground) 20%, var(--border)); color: var(--foreground); }
.share-source__link-icon { flex: 0 0 auto; }
.share-source__button:hover { background: var(--primary-hover); }
@media (max-width: 640px) { .share-source { max-width: 100%; }
.share-source__form { right: 0; width: auto; } }</style>