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
add PDS favicon
handle.invalid
19 hours ago
8f534482
a7420ccf
verified
This commit was signed with the committer's
known signature
.
handle.invalid
SSH Key Fingerprint:
SHA256:mBrT4x0JdzLpbVR95g1hjI1aaErfC02kmLRkPXwsYCk=
+90
-71
6 changed files
expand all
collapse all
unified
split
src
components
backlinks.tsx
favicon.tsx
navbar.tsx
views
car
explore.tsx
record.tsx
repo.tsx
+1
-1
src/components/backlinks.tsx
···
151
151
onClick={() => setExpanded(!expanded())}
152
152
>
153
153
<div class="flex min-w-0 flex-1 items-center gap-2">
154
154
-
<Favicon authority={authority()} />
154
154
+
<Favicon domain={authority()} reverse />
155
155
<div class="flex min-w-0 flex-1 flex-col">
156
156
<span class="w-full truncate">{props.collection}</span>
157
157
<span class="w-full text-xs wrap-break-word text-neutral-500 dark:text-neutral-400">
+3
-2
src/components/favicon.tsx
···
1
1
import { createSignal, JSX, Match, Show, Switch } from "solid-js";
2
2
3
3
export const Favicon = (props: {
4
4
-
authority: string;
4
4
+
domain: string;
5
5
+
reverse?: boolean;
5
6
wrapper?: (children: JSX.Element) => JSX.Element;
6
7
}) => {
7
8
const [loaded, setLoaded] = createSignal(false);
8
8
-
const domain = () => props.authority.split(".").reverse().join(".");
9
9
+
const domain = () => (props.reverse ? props.domain.split(".").reverse().join(".") : props.domain);
9
10
10
11
const content = (
11
12
<Switch>
+81
-58
src/components/navbar.tsx
···
1
1
import * as TID from "@atcute/tid";
2
2
import { A, Params } from "@solidjs/router";
3
3
-
import { createEffect, createMemo, createSignal, Show } from "solid-js";
3
3
+
import { createEffect, createMemo, createSignal, JSX, Match, Show, Switch } from "solid-js";
4
4
import { canHover } from "../layout";
5
5
import { didDocCache } from "../utils/api";
6
6
import { addToClipboard } from "../utils/copy";
7
7
import { localDateFromTimestamp } from "../utils/date";
8
8
-
import { Favicon } from "./favicon";
9
8
import Tooltip from "./tooltip";
10
9
11
10
export const [pds, setPDS] = createSignal<string>();
···
31
30
);
32
31
};
33
32
33
33
+
const HoverFavicon = (props: { domain: string; hovered: boolean; children: JSX.Element }) => {
34
34
+
const [hasHovered, setHasHovered] = createSignal(false);
35
35
+
const [loaded, setLoaded] = createSignal(false);
36
36
+
37
37
+
createEffect(() => {
38
38
+
props.domain;
39
39
+
setHasHovered(false);
40
40
+
setLoaded(false);
41
41
+
});
42
42
+
43
43
+
createEffect(() => {
44
44
+
if (props.hovered) setHasHovered(true);
45
45
+
});
46
46
+
47
47
+
return (
48
48
+
<div class="relative flex h-5 w-3.5 shrink-0 items-center justify-center sm:w-4">
49
49
+
<Show when={!props.hovered || !loaded()}>{props.children}</Show>
50
50
+
<Show when={hasHovered()}>
51
51
+
<Switch>
52
52
+
<Match when={props.domain === "tangled.sh" || props.domain === "tangled.org"}>
53
53
+
<span
54
54
+
class="iconify i-tangled size-4"
55
55
+
classList={{ hidden: !props.hovered || !loaded() }}
56
56
+
ref={() => setLoaded(true)}
57
57
+
/>
58
58
+
</Match>
59
59
+
<Match when={true}>
60
60
+
<img
61
61
+
src={
62
62
+
["bsky.app", "bsky.chat"].includes(props.domain) ?
63
63
+
"https://web-cdn.bsky.app/static/apple-touch-icon.png"
64
64
+
: `https://${props.domain}/favicon.ico`
65
65
+
}
66
66
+
class="size-4"
67
67
+
classList={{ hidden: !props.hovered || !loaded() }}
68
68
+
onLoad={() => setLoaded(true)}
69
69
+
onError={() => setLoaded(false)}
70
70
+
/>
71
71
+
</Match>
72
72
+
</Switch>
73
73
+
</Show>
74
74
+
</div>
75
75
+
);
76
76
+
};
77
77
+
34
78
export const NavBar = (props: { params: Params }) => {
35
79
const [handle, setHandle] = createSignal(props.params.repo);
80
80
+
const [pdsHovered, setPdsHovered] = createSignal(false);
36
81
const [repoHovered, setRepoHovered] = createSignal(false);
37
37
-
const [hasHoveredRepo, setHasHoveredRepo] = createSignal(false);
38
38
-
const [faviconLoaded, setFaviconLoaded] = createSignal(false);
39
82
const [collectionHovered, setCollectionHovered] = createSignal(false);
40
83
const isCustomDomain = () => handle() && !handle()!.endsWith(".bsky.social");
41
84
···
49
92
}
50
93
});
51
94
52
52
-
createEffect(() => {
53
53
-
handle();
54
54
-
setHasHoveredRepo(false);
55
55
-
setFaviconLoaded(false);
56
56
-
});
57
57
-
58
95
const rkeyTimestamp = createMemo(() => {
59
96
if (!props.params.rkey || !TID.validate(props.params.rkey)) return undefined;
60
97
const timestamp = TID.parse(props.params.rkey).timestamp / 1000;
···
64
101
return (
65
102
<nav class="flex w-full flex-col text-sm wrap-anywhere sm:text-base">
66
103
{/* PDS Level */}
67
67
-
<div class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40">
104
104
+
<div
105
105
+
class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40"
106
106
+
onMouseEnter={() => {
107
107
+
if (canHover) setPdsHovered(true);
108
108
+
}}
109
109
+
onMouseLeave={() => {
110
110
+
if (canHover) setPdsHovered(false);
111
111
+
}}
112
112
+
>
68
113
<div class="flex min-h-6 basis-full items-center gap-2 sm:min-h-7">
69
114
<Tooltip text="PDS">
70
70
-
<span
71
71
-
classList={{
72
72
-
"iconify shrink-0 transition-colors duration-200": true,
73
73
-
"lucide--unplug text-red-500 dark:text-red-400":
74
74
-
pds() === "Missing PDS" && props.params.repo?.startsWith("did:"),
75
75
-
"lucide--hard-drive text-neutral-500 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200":
76
76
-
pds() !== "Missing PDS" || !props.params.repo?.startsWith("did:"),
77
77
-
}}
78
78
-
></span>
115
115
+
<HoverFavicon
116
116
+
domain={pds() ?? ""}
117
117
+
hovered={pdsHovered() && !!pds() && pds() !== "Missing PDS"}
118
118
+
>
119
119
+
<span
120
120
+
classList={{
121
121
+
"iconify shrink-0 transition-colors duration-200": true,
122
122
+
"lucide--unplug text-red-500 dark:text-red-400":
123
123
+
pds() === "Missing PDS" && props.params.repo?.startsWith("did:"),
124
124
+
"lucide--hard-drive text-neutral-500 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200":
125
125
+
pds() !== "Missing PDS" || !props.params.repo?.startsWith("did:"),
126
126
+
}}
127
127
+
></span>
128
128
+
</HoverFavicon>
79
129
</Tooltip>
80
130
<Show when={pds() && (pds() !== "Missing PDS" || props.params.repo?.startsWith("did:"))}>
81
131
<Show
···
110
160
<div
111
161
class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40"
112
162
onMouseEnter={() => {
113
113
-
if (canHover) {
114
114
-
setRepoHovered(true);
115
115
-
setHasHoveredRepo(true);
116
116
-
}
163
163
+
if (canHover) setRepoHovered(true);
117
164
}}
118
165
onMouseLeave={() => {
119
119
-
if (canHover) {
120
120
-
setRepoHovered(false);
121
121
-
}
166
166
+
if (canHover) setRepoHovered(false);
122
167
}}
123
168
>
124
169
<div class="flex min-w-0 basis-full items-center gap-2">
125
170
<Tooltip text="Repository">
126
126
-
<div class="relative flex h-5 w-3.5 shrink-0 items-center justify-center sm:w-4">
127
127
-
<span
128
128
-
class="iconify lucide--book-user absolute text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"
129
129
-
classList={{
130
130
-
hidden: !!(repoHovered() && isCustomDomain() && faviconLoaded()),
131
131
-
}}
132
132
-
></span>
133
133
-
<Show when={hasHoveredRepo() && isCustomDomain()}>
134
134
-
<img
135
135
-
src={`https://${handle()}/favicon.ico`}
136
136
-
class="size-4"
137
137
-
classList={{ hidden: !repoHovered() || !faviconLoaded() }}
138
138
-
onLoad={() => setFaviconLoaded(true)}
139
139
-
onError={() => setFaviconLoaded(false)}
140
140
-
/>
141
141
-
</Show>
142
142
-
</div>
171
171
+
<HoverFavicon domain={handle() ?? ""} hovered={repoHovered() && !!isCustomDomain()}>
172
172
+
<span class="iconify lucide--book-user text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span>
173
173
+
</HoverFavicon>
143
174
</Tooltip>
144
175
<Show
145
176
when={props.params.collection}
···
189
220
>
190
221
<div class="flex basis-full items-center gap-2">
191
222
<Tooltip text="Collection">
192
192
-
<div class="relative flex h-5 w-3.5 shrink-0 items-center justify-center sm:w-4">
193
193
-
<Show
194
194
-
when={collectionHovered()}
195
195
-
fallback={
196
196
-
<span class="iconify lucide--folder-open text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span>
197
197
-
}
198
198
-
>
199
199
-
{(() => {
200
200
-
const parts = props.params.collection!.split(".");
201
201
-
const authority = `${parts[0]}.${parts[1]}`;
202
202
-
return <Favicon authority={authority} wrapper={(c) => c} />;
203
203
-
})()}
204
204
-
</Show>
205
205
-
</div>
223
223
+
<HoverFavicon
224
224
+
domain={props.params.collection!.split(".").slice(0, 2).reverse().join(".")}
225
225
+
hovered={collectionHovered()}
226
226
+
>
227
227
+
<span class="iconify lucide--folder-open text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span>
228
228
+
</HoverFavicon>
206
229
</Tooltip>
207
230
<Show
208
231
when={props.params.rkey}
+1
-1
src/views/car/explore.tsx
···
437
437
}}
438
438
class="flex w-full items-center gap-2 rounded p-2 text-left text-sm hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-800 dark:active:bg-neutral-700"
439
439
>
440
440
-
<Favicon authority={authority()} />
440
440
+
<Favicon domain={authority()} reverse />
441
441
<span
442
442
class="truncate font-medium"
443
443
classList={{
+2
-8
src/views/record.tsx
···
33
33
import { AtUri, uriTemplates } from "../utils/templates.js";
34
34
import { lexicons } from "../utils/types/lexicons.js";
35
35
36
36
-
const toAuthority = (hostname: string) => hostname.split(".").reverse().join(".");
37
37
-
38
36
const faviconWrapper = (children: any) => (
39
37
<div class="flex size-4 items-center justify-center">{children}</div>
40
38
);
···
493
491
}}
494
492
>
495
493
<Favicon
496
496
-
authority={toAuthority(new URL(link().link).hostname)}
494
494
+
domain={new URL(link().link).hostname}
497
495
wrapper={faviconWrapper}
498
496
/>
499
497
</a>
···
512
510
>
513
511
{alt.icon ?
514
512
<img src={alt.icon} class="size-4" />
515
515
-
: <Favicon
516
516
-
authority={toAuthority(alt.hostname)}
517
517
-
wrapper={faviconWrapper}
518
518
-
/>
519
519
-
}
513
513
+
: <Favicon domain={alt.hostname} wrapper={faviconWrapper} />}
520
514
</a>
521
515
)}
522
516
</For>
+2
-1
src/views/repo.tsx
···
476
476
}}
477
477
>
478
478
<Favicon
479
479
-
authority={authority}
479
479
+
domain={authority}
480
480
+
reverse
480
481
wrapper={(children) => (
481
482
<a
482
483
href={`#collections:${authority}`}