atproto explorer

sticky component

+51 -49
+44
src/components/sticky.tsx
··· 1 + import { createSignal, JSX, onCleanup, onMount } from "solid-js"; 2 + 3 + export const StickyOverlay = (props: { children?: JSX.Element }) => { 4 + const [filterStuck, setFilterStuck] = createSignal(false); 5 + 6 + return ( 7 + <div 8 + ref={(node) => { 9 + onMount(() => { 10 + let ticking = false; 11 + const tick = () => { 12 + const topPx = parseFloat(getComputedStyle(node).top); 13 + const { top } = node.getBoundingClientRect(); 14 + setFilterStuck(top <= topPx + 0.5); 15 + ticking = false; 16 + }; 17 + 18 + const onScroll = () => { 19 + if (!ticking) { 20 + ticking = true; 21 + requestAnimationFrame(tick); 22 + } 23 + }; 24 + 25 + window.addEventListener("scroll", onScroll, { passive: true }); 26 + 27 + tick(); 28 + 29 + onCleanup(() => { 30 + window.removeEventListener("scroll", onScroll); 31 + }); 32 + }); 33 + }} 34 + class="sticky top-2 z-10 flex flex-col items-center justify-center gap-2 rounded-lg p-3 transition-colors" 35 + classList={{ 36 + "bg-neutral-50 dark:bg-dark-300 border-[0.5px] border-neutral-300 dark:border-neutral-700 shadow-md": 37 + filterStuck(), 38 + "bg-transparent border-transparent shadow-none": !filterStuck(), 39 + }} 40 + > 41 + {props.children} 42 + </div> 43 + ); 44 + };
+4 -47
src/views/collection.tsx
··· 3 3 import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons"; 4 4 import * as TID from "@atcute/tid"; 5 5 import { A, useParams } from "@solidjs/router"; 6 - import { 7 - createEffect, 8 - createResource, 9 - createSignal, 10 - For, 11 - onCleanup, 12 - onMount, 13 - Show, 14 - untrack, 15 - } from "solid-js"; 6 + import { createEffect, createResource, createSignal, For, Show, untrack } from "solid-js"; 16 7 import { createStore } from "solid-js/store"; 17 8 import { Button } from "../components/button.jsx"; 18 9 import { JSONType, JSONValue } from "../components/json.jsx"; 19 10 import { agent } from "../components/login.jsx"; 11 + import { StickyOverlay } from "../components/sticky.jsx"; 20 12 import { TextInput } from "../components/text-input.jsx"; 21 13 import Tooltip from "../components/tooltip.jsx"; 22 14 import { setNotif } from "../layout.jsx"; ··· 81 73 const [batchDelete, setBatchDelete] = createSignal(false); 82 74 const [lastSelected, setLastSelected] = createSignal<number>(); 83 75 const [reverse, setReverse] = createSignal(false); 84 - const [filterStuck, setFilterStuck] = createSignal(false); 85 76 const did = params.repo; 86 77 let pds: string; 87 78 let rpc: Client; 88 - let sticky!: HTMLDivElement; 89 79 90 80 const fetchRecords = async () => { 91 81 if (!pds) pds = await resolvePDS(did); ··· 168 158 true, 169 159 ); 170 160 171 - onMount(() => { 172 - let ticking = false; 173 - const tick = () => { 174 - const topPx = parseFloat(getComputedStyle(sticky).top); 175 - const { top } = sticky.getBoundingClientRect(); 176 - setFilterStuck(top <= topPx + 0.5); 177 - ticking = false; 178 - }; 179 - 180 - const onScroll = () => { 181 - if (!ticking) { 182 - ticking = true; 183 - requestAnimationFrame(tick); 184 - } 185 - }; 186 - 187 - window.addEventListener("scroll", onScroll, { passive: true }); 188 - 189 - tick(); 190 - 191 - onCleanup(() => { 192 - window.removeEventListener("scroll", onScroll); 193 - }); 194 - }); 195 - 196 161 return ( 197 162 <Show when={records.length || response()}> 198 163 <div class="-mt-2 flex w-full flex-col items-center"> 199 - <div 200 - ref={(el) => (sticky = el)} 201 - class="sticky top-2 z-10 flex flex-col items-center justify-center gap-2 rounded-lg p-3 transition-colors" 202 - classList={{ 203 - "bg-neutral-50 dark:bg-dark-300 border-[0.5px] border-neutral-300 dark:border-neutral-700 shadow-md": 204 - filterStuck(), 205 - "bg-transparent border-transparent shadow-none": !filterStuck(), 206 - }} 207 - > 164 + <StickyOverlay> 208 165 <div class="flex w-[22rem] items-center gap-2 sm:w-[24rem]"> 209 166 <Show when={agent() && agent()?.sub === did}> 210 167 <div class="flex items-center gap-x-2"> ··· 297 254 </div> 298 255 </div> 299 256 </Show> 300 - </div> 257 + </StickyOverlay> 301 258 <div class="flex max-w-full flex-col font-mono"> 302 259 <For 303 260 each={records.filter((rec) =>
+3 -2
src/views/labels.tsx
··· 3 3 import { A, useParams, useSearchParams } from "@solidjs/router"; 4 4 import { createResource, createSignal, For, onMount, Show } from "solid-js"; 5 5 import { Button } from "../components/button.jsx"; 6 + import { StickyOverlay } from "../components/sticky.jsx"; 6 7 import { TextInput } from "../components/text-input.jsx"; 7 8 import { labelerCache, resolvePDS } from "../utils/api.js"; 8 9 import { localDateFromTimestamp } from "../utils/date.js"; ··· 98 99 </div> 99 100 </div> 100 101 </form> 101 - <div class="dark:bg-dark-500 sticky top-0 z-5 flex w-screen flex-col items-center justify-center gap-3 bg-neutral-100 py-3"> 102 + <StickyOverlay> 102 103 <TextInput 103 104 placeholder="Filter by label" 104 105 onInput={(e) => setFilter(e.currentTarget.value)} ··· 123 124 </div> 124 125 </Show> 125 126 </div> 126 - </div> 127 + </StickyOverlay> 127 128 <Show when={labels().length}> 128 129 <div class="flex max-w-full min-w-[22rem] flex-col gap-2 divide-y-[0.5px] divide-neutral-400 text-sm wrap-anywhere whitespace-pre-wrap sm:min-w-[24rem] dark:divide-neutral-600"> 129 130 <For each={filterLabels()}>