tangled
alpha
login
or
join now
pds.ls
/
pdsls
398
fork
atom
atmosphere explorer
pds.ls
tool
typescript
atproto
398
fork
atom
overview
issues
1
pulls
pipelines
labels bottom bar
handle.invalid
1 week ago
2a2c4343
1bb22c59
verified
This commit was signed with the committer's
known signature
.
handle.invalid
SSH Key Fingerprint:
SHA256:mBrT4x0JdzLpbVR95g1hjI1aaErfC02kmLRkPXwsYCk=
+87
-109
5 changed files
expand all
collapse all
unified
split
src
components
sticky.tsx
utils
keyboard.ts
views
collection.tsx
labels.tsx
repo.tsx
-43
src/components/sticky.tsx
···
1
1
-
import { createSignal, JSX, onCleanup, onMount } from "solid-js";
2
2
-
3
3
-
export const StickyOverlay = (props: { children?: JSX.Element }) => {
4
4
-
const [filterStuck, setFilterStuck] = createSignal(false);
5
5
-
6
6
-
return (
7
7
-
<>
8
8
-
<div
9
9
-
ref={(trigger) => {
10
10
-
onMount(() => {
11
11
-
const observer = new IntersectionObserver(
12
12
-
([entry]) => setFilterStuck(!entry.isIntersecting),
13
13
-
{
14
14
-
rootMargin: "-8px 0px 0px 0px",
15
15
-
threshold: 0,
16
16
-
},
17
17
-
);
18
18
-
19
19
-
observer.observe(trigger);
20
20
-
21
21
-
onCleanup(() => {
22
22
-
observer.unobserve(trigger);
23
23
-
observer.disconnect();
24
24
-
});
25
25
-
});
26
26
-
}}
27
27
-
class="pointer-events-none h-0"
28
28
-
aria-hidden="true"
29
29
-
/>
30
30
-
31
31
-
<div
32
32
-
class="sticky top-2 z-10 flex w-full flex-col items-center justify-center gap-2 rounded-lg border-[0.5px] p-3 transition-colors"
33
33
-
classList={{
34
34
-
"bg-neutral-50 dark:bg-dark-300 border-neutral-300 dark:border-neutral-700 shadow-md":
35
35
-
filterStuck(),
36
36
-
"bg-transparent border-transparent shadow-none": !filterStuck(),
37
37
-
}}
38
38
-
>
39
39
-
{props.children}
40
40
-
</div>
41
41
-
</>
42
42
-
);
43
43
-
};
+16
src/utils/keyboard.ts
···
1
1
+
import { onCleanup } from "solid-js";
2
2
+
3
3
+
export const useFilterShortcut = (getRef: () => HTMLInputElement | undefined) => {
4
4
+
const handleKeyDown = (e: KeyboardEvent) => {
5
5
+
if (
6
6
+
e.key === "/" &&
7
7
+
!["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName) &&
8
8
+
!document.querySelector("[data-modal]")
9
9
+
) {
10
10
+
e.preventDefault();
11
11
+
getRef()?.focus();
12
12
+
}
13
13
+
};
14
14
+
document.addEventListener("keydown", handleKeyDown);
15
15
+
onCleanup(() => document.removeEventListener("keydown", handleKeyDown));
16
16
+
};
+3
-13
src/views/collection.tsx
···
4
4
import * as TID from "@atcute/tid";
5
5
import { Title } from "@solidjs/meta";
6
6
import { A, useBeforeLeave, useParams, useSearchParams } from "@solidjs/router";
7
7
-
import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js";
7
7
+
import { createMemo, createResource, createSignal, For, onMount, Show } from "solid-js";
8
8
import { createStore } from "solid-js/store";
9
9
import { agent } from "../auth/state";
10
10
import { Button } from "../components/button.jsx";
···
17
17
import { canHover } from "../layout.jsx";
18
18
import { resolvePDS } from "../utils/api.js";
19
19
import { localDateFromTimestamp } from "../utils/date.js";
20
20
+
import { useFilterShortcut } from "../utils/keyboard.js";
20
21
import {
21
22
clearCollectionCache,
22
23
getCollectionCache,
···
104
105
});
105
106
}
106
107
107
107
-
const handleKeyDown = (e: KeyboardEvent) => {
108
108
-
if (
109
109
-
e.key === "/" &&
110
110
-
!["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName) &&
111
111
-
!document.querySelector("[data-modal]")
112
112
-
) {
113
113
-
e.preventDefault();
114
114
-
filterInputRef?.focus();
115
115
-
}
116
116
-
};
117
117
-
document.addEventListener("keydown", handleKeyDown);
118
118
-
onCleanup(() => document.removeEventListener("keydown", handleKeyDown));
108
108
+
useFilterShortcut(() => filterInputRef);
119
109
});
120
110
121
111
useBeforeLeave((e) => {
+66
-39
src/views/labels.tsx
···
8
8
import { Button } from "../components/button.jsx";
9
9
import DidHoverCard from "../components/hover-card/did.jsx";
10
10
import RecordHoverCard from "../components/hover-card/record.jsx";
11
11
-
import { StickyOverlay } from "../components/sticky.jsx";
12
11
import { TextInput } from "../components/text-input.jsx";
12
12
+
import { canHover } from "../layout.jsx";
13
13
import { labelerCache, resolveHandle, resolvePDS } from "../utils/api.js";
14
14
import { localDateFromTimestamp } from "../utils/date.js";
15
15
+
import { useFilterShortcut } from "../utils/keyboard.js";
15
16
16
17
const LABELS_PER_PAGE = 50;
17
18
const DEFAULT_LABELER_DID = "did:plc:ar7c4by46qjdydhdevvrndac";
···
67
68
68
69
let rpc: Client | undefined;
69
70
let formRef!: HTMLFormElement;
71
71
+
let filterInputRef: HTMLInputElement | undefined;
70
72
71
73
const filteredLabels = createMemo(() => {
72
74
const filterValue = filter().trim();
···
116
118
const hasSearched = createMemo(() => Boolean(searchParams.uriPatterns));
117
119
118
120
onMount(async () => {
121
121
+
useFilterShortcut(() => filterInputRef);
122
122
+
119
123
if (searchParams.did && searchParams.uriPatterns) {
120
124
const formData = new FormData();
121
125
formData.append("did", searchParams.did.toString());
···
128
132
let did = formData.get("did")?.toString()?.trim() || DEFAULT_LABELER_DID;
129
133
const uriPatterns = formData.get("uriPatterns")?.toString()?.trim();
130
134
131
131
-
if (!did || !uriPatterns) {
135
135
+
if (!uriPatterns) {
132
136
setError("Please provide both DID and URI patterns");
133
137
return;
134
138
}
···
244
248
</form>
245
249
246
250
<Show when={hasSearched()}>
247
247
-
<StickyOverlay>
248
248
-
<div class="flex w-full items-center gap-x-2">
249
249
-
<TextInput
250
250
-
placeholder="Filter labels (* for partial, -exclude)"
251
251
-
name="filter"
252
252
-
value={filter()}
253
253
-
onInput={(e) => setFilter(e.currentTarget.value)}
254
254
-
class="min-w-0 grow text-sm placeholder:text-xs"
255
255
-
/>
256
256
-
<div class="flex shrink-0 items-center gap-x-2 text-sm">
257
257
-
<Show when={labels().length > 0}>
258
258
-
<span class="whitespace-nowrap text-neutral-600 dark:text-neutral-400">
259
259
-
{filteredLabels().length}/{labels().length}
260
260
-
</span>
261
261
-
</Show>
262
262
-
263
263
-
<Show when={cursor()}>
264
264
-
<Button
265
265
-
onClick={handleLoadMore}
266
266
-
disabled={loading()}
267
267
-
classList={{ "w-20 h-7.5 justify-center": true }}
268
268
-
>
269
269
-
<Show
270
270
-
when={!loading()}
271
271
-
fallback={
272
272
-
<span class="iconify lucide--loader-circle animate-spin text-base" />
273
273
-
}
274
274
-
>
275
275
-
Load more
276
276
-
</Show>
277
277
-
</Button>
278
278
-
</Show>
279
279
-
</div>
280
280
-
</div>
281
281
-
</StickyOverlay>
282
282
-
283
283
-
<div class="w-full max-w-3xl py-2">
251
251
+
<div class="w-full max-w-3xl py-2 pb-20">
284
252
<Show when={loading() && labels().length === 0}>
285
253
<div class="flex flex-col items-center justify-center py-12 text-center">
286
254
<span class="iconify lucide--loader-circle mb-3 animate-spin text-4xl text-neutral-400" />
···
311
279
</div>
312
280
</Show>
313
281
</Show>
282
282
+
</div>
283
283
+
284
284
+
<div class="dark:bg-dark-500 fixed bottom-0 z-10 flex w-full flex-col items-center gap-2 border-t border-neutral-200 bg-neutral-100 px-3 pt-3 pb-6 dark:border-neutral-700">
285
285
+
<div
286
286
+
class="dark:bg-dark-200 flex w-full max-w-lg cursor-text items-center gap-2 rounded-lg border border-neutral-200 bg-white px-3 dark:border-neutral-700"
287
287
+
onClick={(e) => {
288
288
+
const input = e.currentTarget.querySelector("input");
289
289
+
if (e.target !== input) input?.focus();
290
290
+
}}
291
291
+
>
292
292
+
<span class="iconify lucide--filter text-neutral-500 dark:text-neutral-400" />
293
293
+
<input
294
294
+
ref={filterInputRef}
295
295
+
type="text"
296
296
+
spellcheck={false}
297
297
+
autocapitalize="off"
298
298
+
autocomplete="off"
299
299
+
class="grow py-2 select-none placeholder:text-sm focus:outline-none"
300
300
+
placeholder="Filter labels... (* for partial, -exclude)"
301
301
+
value={filter()}
302
302
+
onInput={(e) => setFilter(e.currentTarget.value)}
303
303
+
/>
304
304
+
<Show when={canHover && !filter()}>
305
305
+
<kbd class="rounded border border-neutral-200 bg-neutral-50 px-1.5 py-0.5 font-mono text-xs text-neutral-400 select-none dark:border-neutral-600 dark:bg-neutral-700">
306
306
+
/
307
307
+
</kbd>
308
308
+
</Show>
309
309
+
</div>
310
310
+
311
311
+
<div class="flex min-h-7.5 w-full max-w-lg items-center justify-between">
312
312
+
<div class="w-20" />
313
313
+
314
314
+
<div>
315
315
+
<Show when={filter()}>
316
316
+
<span>{filteredLabels().length}</span>
317
317
+
<span>/</span>
318
318
+
</Show>
319
319
+
<span>{labels().length} labels</span>
320
320
+
</div>
321
321
+
322
322
+
<div class="flex w-20 items-center justify-end">
323
323
+
<Show when={cursor()}>
324
324
+
<Button
325
325
+
onClick={handleLoadMore}
326
326
+
disabled={loading()}
327
327
+
classList={{ "w-20 h-7.5 justify-center": true }}
328
328
+
>
329
329
+
<Show
330
330
+
when={!loading()}
331
331
+
fallback={
332
332
+
<span class="iconify lucide--loader-circle animate-spin text-base" />
333
333
+
}
334
334
+
>
335
335
+
Load more
336
336
+
</Show>
337
337
+
</Button>
338
338
+
</Show>
339
339
+
</div>
340
340
+
</div>
314
341
</div>
315
342
</Show>
316
343
</div>
+2
-14
src/views/repo.tsx
···
9
9
createSignal,
10
10
ErrorBoundary,
11
11
For,
12
12
-
onCleanup,
13
12
onMount,
14
13
Show,
15
14
Suspense,
···
42
41
validateHandle,
43
42
} from "../utils/api.js";
44
43
import { detectDidKeyType, detectKeyType } from "../utils/key.js";
44
44
+
import { useFilterShortcut } from "../utils/keyboard.js";
45
45
import { BlobView } from "./blob.jsx";
46
46
import { PlcLogView } from "./logs.jsx";
47
47
import { plcDirectory } from "./settings.jsx";
···
80
80
});
81
81
82
82
onMount(() => {
83
83
-
const handleKeyDown = (e: KeyboardEvent) => {
84
84
-
if (
85
85
-
e.key === "/" &&
86
86
-
!["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName) &&
87
87
-
!document.querySelector("[data-modal]")
88
88
-
) {
89
89
-
e.preventDefault();
90
90
-
filterInputRef?.focus();
91
91
-
}
92
92
-
};
93
93
-
94
94
-
document.addEventListener("keydown", handleKeyDown);
95
95
-
onCleanup(() => document.removeEventListener("keydown", handleKeyDown));
83
83
+
useFilterShortcut(() => filterInputRef);
96
84
});
97
85
98
86
const RepoTab = (props: {