+22
.tangled/workflows/deploy.yml
+22
.tangled/workflows/deploy.yml
···
···
1
+
## need this for commit idk what else to change
2
+
3
+
when:
4
+
- event: ["push"]
5
+
branch: ["main"]
6
+
7
+
engine: "nixery"
8
+
9
+
clone:
10
+
skip: true
11
+
12
+
dependencies:
13
+
nixpkgs:
14
+
- curl
15
+
16
+
steps:
17
+
- name: "Trigger Deploy"
18
+
command: |
19
+
curl -X POST \
20
+
-H "Authorization: Bearer $SCANS_HOST_API_KEY" \
21
+
-H "Authorization: Bearer $SCANS_HOST_API_KEY" \
22
+
https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy?wait=true
+25
0001-ok.patch
+25
0001-ok.patch
···
···
1
+
From baf405c82fb23f9274a35384286bac2b901d45af Mon Sep 17 00:00:00 2001
2
+
From: scanash00 <scan@scanash.com>
3
+
Date: Tue, 30 Dec 2025 22:12:13 -0900
4
+
Subject: [PATCH] add ?wait=true
5
+
6
+
---
7
+
.tangled/workflows/deploy.yml | 3 ++-
8
+
1 file changed, 2 insertions(+), 1 deletion(-)
9
+
10
+
diff --git a/.tangled/workflows/deploy.yml b/.tangled/workflows/deploy.yml
11
+
index a44c51b..b7edfea 100644
12
+
--- a/.tangled/workflows/deploy.yml
13
+
+++ b/.tangled/workflows/deploy.yml
14
+
@@ -16,4 +16,5 @@ steps:
15
+
command: |
16
+
curl -X POST \
17
+
-H "Authorization: Bearer $SCANS_HOST_API_KEY" \
18
+
- https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy
19
+
\ No newline at end of file
20
+
+ -H "Authorization: Bearer $SCANS_HOST_API_KEY" \
21
+
+ https://free.scan.blue/api/v1/sites/YOUR_SITE_ID/deploy?wait=true
22
+
\ No newline at end of file
23
+
--
24
+
2.50.1 (Apple Git-155)
25
+
+1
-1
src/auth/account.tsx
+1
-1
src/auth/account.tsx
···
140
>
141
<span class="truncate">{sessions[did]?.handle || did}</span>
142
<Show when={did === agent()?.sub && sessions[did].signedIn}>
143
-
<span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span>
144
</Show>
145
<Show when={!sessions[did].signedIn}>
146
<span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span>
···
140
>
141
<span class="truncate">{sessions[did]?.handle || did}</span>
142
<Show when={did === agent()?.sub && sessions[did].signedIn}>
143
+
<span class="iconify lucide--circle-check shrink-0 text-blue-500 dark:text-blue-400"></span>
144
</Show>
145
<Show when={!sessions[did].signedIn}>
146
<span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span>
+1
-1
src/auth/login.tsx
+1
-1
src/auth/login.tsx
···
49
<label for="username" class="hidden">
50
Add account
51
</label>
52
-
<div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400">
53
<label
54
for="username"
55
class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400"
···
49
<label for="username" class="hidden">
50
Add account
51
</label>
52
+
<div class="dark:bg-dark-100 flex grow items-center gap-2 rounded-lg bg-white px-2 outline-1 outline-neutral-200 focus-within:outline-[1.5px] focus-within:outline-neutral-600 dark:outline-neutral-600 dark:focus-within:outline-neutral-400">
53
<label
54
for="username"
55
class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400"
+1
-1
src/components/backlinks.tsx
+1
-1
src/components/backlinks.tsx
···
51
return (
52
<a
53
href={`/at://${did}/${collection}/${rkey}`}
54
-
class="grid grid-cols-[auto_1fr_auto] items-center gap-x-1 px-2 py-1.5 font-mono text-xs hover:bg-neutral-200/50 sm:gap-x-3 sm:px-3 dark:hover:bg-neutral-700/50"
55
>
56
<span class="text-blue-500 dark:text-blue-400">{rkey}</span>
57
<span class="truncate text-neutral-700 dark:text-neutral-300" title={did}>
···
51
return (
52
<a
53
href={`/at://${did}/${collection}/${rkey}`}
54
+
class="grid grid-cols-[auto_1fr_auto] items-center gap-x-1 px-2 py-1.5 font-mono text-xs select-none hover:bg-neutral-200/50 active:bg-neutral-200/50 sm:gap-x-3 sm:px-3 dark:hover:bg-neutral-700/50 dark:active:bg-neutral-700/50"
55
>
56
<span class="text-blue-500 dark:text-blue-400">{rkey}</span>
57
<span class="truncate text-neutral-700 dark:text-neutral-300" title={did}>
+7
-2
src/components/dropdown.tsx
+7
-2
src/components/dropdown.tsx
···
75
export const ActionMenu = (props: {
76
label: string;
77
icon: string;
78
-
onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
79
}) => {
80
return (
81
<button
82
-
onClick={props.onClick}
83
class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
84
>
85
<Show when={props.icon}>
···
75
export const ActionMenu = (props: {
76
label: string;
77
icon: string;
78
+
onClick: () => void;
79
}) => {
80
+
const ctx = useContext(MenuContext);
81
+
82
return (
83
<button
84
+
onClick={() => {
85
+
props.onClick();
86
+
ctx?.setShowMenu(false);
87
+
}}
88
class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
89
>
90
<Show when={props.icon}>
+70
-56
src/components/json.tsx
+70
-56
src/components/json.tsx
···
1
import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax";
2
import { A, useNavigate, useParams } from "@solidjs/router";
3
-
import { createEffect, createSignal, ErrorBoundary, For, on, Show } from "solid-js";
4
import { resolveLexiconAuthority } from "../utils/api";
5
import { hideMedia } from "../views/settings";
6
import { pds } from "./navbar";
7
import { addNotification, removeNotification } from "./notification";
8
import VideoPlayer from "./video-player";
9
10
interface AtBlob {
11
$type: string;
12
ref: { $link: string };
13
mimeType: string;
14
}
15
16
-
const JSONString = (props: {
17
-
data: string;
18
-
isType?: boolean;
19
-
isLink?: boolean;
20
-
parentIsBlob?: boolean;
21
-
}) => {
22
const navigate = useNavigate();
23
const params = useParams();
24
25
-
const isURL =
26
-
URL.canParse ??
27
-
((url, base) => {
28
-
try {
29
-
new URL(url, base);
30
-
return true;
31
-
} catch {
32
-
return false;
33
-
}
34
-
});
35
-
36
const handleClick = async (lex: string) => {
37
try {
38
const [nsid, anchor] = lex.split("#");
···
50
}
51
};
52
53
return (
54
<span>
55
"
56
-
<For each={props.data.split(/(\s)/)}>
57
{(part) => (
58
<>
59
{isResourceUri(part) ?
···
72
>
73
{part}
74
</button>
75
-
: isCid(part) && props.isLink && props.parentIsBlob && params.repo ?
76
<A
77
class="text-blue-400 hover:underline active:underline"
78
rel="noopener"
···
93
</>
94
)}
95
</For>
96
"
97
</span>
98
);
99
};
···
110
return <span>null</span>;
111
};
112
113
-
const JSONObject = (props: {
114
-
data: { [x: string]: JSONType };
115
-
repo: string;
116
-
parentIsBlob?: boolean;
117
-
}) => {
118
const params = useParams();
119
const [hide, setHide] = createSignal(
120
localStorage.hideMedia === "true" || params.rkey === undefined,
···
136
);
137
138
const isBlob = props.data.$type === "blob";
139
-
const isBlobContext = isBlob || props.parentIsBlob;
140
141
const Obj = ({ key, value }: { key: string; value: JSONType }) => {
142
const [show, setShow] = createSignal(true);
···
169
"self-center": value !== Object(value),
170
"pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 transition-colors dark:has-hover:group-hover/indent:border-neutral-300":
171
value === Object(value),
172
-
"invisible h-0": !show(),
173
}}
174
>
175
-
<JSONValue
176
-
data={value}
177
-
repo={props.repo}
178
-
isType={key === "$type"}
179
-
isLink={key === "$link"}
180
-
parentIsBlob={isBlobContext}
181
-
/>
182
</span>
183
</span>
184
);
···
200
<Show when={blob.mimeType.startsWith("image/")}>
201
<img
202
class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64"
203
-
src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${blob.ref.$link}`}
204
onLoad={() => setMediaLoaded(true)}
205
/>
206
</Show>
207
<Show when={blob.mimeType === "video/mp4"}>
208
<ErrorBoundary fallback={() => <span>Failed to load video</span>}>
209
<VideoPlayer
210
-
did={props.repo}
211
cid={blob.ref.$link}
212
onLoad={() => setMediaLoaded(true)}
213
/>
···
241
return rawObj;
242
};
243
244
-
const JSONArray = (props: { data: JSONType[]; repo: string; parentIsBlob?: boolean }) => {
245
return (
246
<For each={props.data}>
247
{(value, index) => (
···
252
}}
253
>
254
<span class="ml-[1ch] w-full">
255
-
<JSONValue data={value} repo={props.repo} parentIsBlob={props.parentIsBlob} />
256
</span>
257
</span>
258
)}
···
260
);
261
};
262
263
-
export const JSONValue = (props: {
264
-
data: JSONType;
265
-
repo: string;
266
-
isType?: boolean;
267
-
isLink?: boolean;
268
-
parentIsBlob?: boolean;
269
-
}) => {
270
const data = props.data;
271
if (typeof data === "string")
272
-
return (
273
-
<JSONString
274
-
data={data}
275
-
isType={props.isType}
276
-
isLink={props.isLink}
277
-
parentIsBlob={props.parentIsBlob}
278
-
/>
279
-
);
280
if (typeof data === "number") return <JSONNumber data={data} />;
281
if (typeof data === "boolean") return <JSONBoolean data={data} />;
282
if (data === null) return <JSONNull />;
283
-
if (Array.isArray(data))
284
-
return <JSONArray data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />;
285
-
return <JSONObject data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />;
286
};
287
288
export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
···
1
import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax";
2
import { A, useNavigate, useParams } from "@solidjs/router";
3
+
import {
4
+
createContext,
5
+
createEffect,
6
+
createSignal,
7
+
ErrorBoundary,
8
+
For,
9
+
on,
10
+
Show,
11
+
useContext,
12
+
} from "solid-js";
13
import { resolveLexiconAuthority } from "../utils/api";
14
import { hideMedia } from "../views/settings";
15
import { pds } from "./navbar";
16
import { addNotification, removeNotification } from "./notification";
17
import VideoPlayer from "./video-player";
18
19
+
interface JSONContext {
20
+
repo: string;
21
+
truncate?: boolean;
22
+
parentIsBlob?: boolean;
23
+
}
24
+
25
+
const JSONCtx = createContext<JSONContext>();
26
+
const useJSONCtx = () => useContext(JSONCtx)!;
27
+
28
interface AtBlob {
29
$type: string;
30
ref: { $link: string };
31
mimeType: string;
32
}
33
34
+
const isURL =
35
+
URL.canParse ??
36
+
((url, base) => {
37
+
try {
38
+
new URL(url, base);
39
+
return true;
40
+
} catch {
41
+
return false;
42
+
}
43
+
});
44
+
45
+
const JSONString = (props: { data: string; isType?: boolean; isLink?: boolean }) => {
46
+
const ctx = useJSONCtx();
47
const navigate = useNavigate();
48
const params = useParams();
49
50
const handleClick = async (lex: string) => {
51
try {
52
const [nsid, anchor] = lex.split("#");
···
64
}
65
};
66
67
+
const MAX_LENGTH = 200;
68
+
const isTruncated = () => ctx.truncate && props.data.length > MAX_LENGTH;
69
+
const displayData = () => (isTruncated() ? props.data.slice(0, MAX_LENGTH) : props.data);
70
+
const remainingChars = () => props.data.length - MAX_LENGTH;
71
+
72
return (
73
<span>
74
"
75
+
<For each={displayData().split(/(\s)/)}>
76
{(part) => (
77
<>
78
{isResourceUri(part) ?
···
91
>
92
{part}
93
</button>
94
+
: isCid(part) && props.isLink && ctx.parentIsBlob && params.repo ?
95
<A
96
class="text-blue-400 hover:underline active:underline"
97
rel="noopener"
···
112
</>
113
)}
114
</For>
115
+
<Show when={isTruncated()}>
116
+
<span>โฆ</span>
117
+
</Show>
118
"
119
+
<Show when={isTruncated()}>
120
+
<span class="ml-1 text-neutral-500 dark:text-neutral-400">
121
+
(+{remainingChars().toLocaleString()})
122
+
</span>
123
+
</Show>
124
</span>
125
);
126
};
···
137
return <span>null</span>;
138
};
139
140
+
const JSONObject = (props: { data: { [x: string]: JSONType } }) => {
141
+
const ctx = useJSONCtx();
142
const params = useParams();
143
const [hide, setHide] = createSignal(
144
localStorage.hideMedia === "true" || params.rkey === undefined,
···
160
);
161
162
const isBlob = props.data.$type === "blob";
163
+
const isBlobContext = isBlob || ctx.parentIsBlob;
164
165
const Obj = ({ key, value }: { key: string; value: JSONType }) => {
166
const [show, setShow] = createSignal(true);
···
193
"self-center": value !== Object(value),
194
"pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 transition-colors dark:has-hover:group-hover/indent:border-neutral-300":
195
value === Object(value),
196
+
"invisible h-0 overflow-hidden": !show(),
197
}}
198
>
199
+
<JSONCtx.Provider value={{ ...ctx, parentIsBlob: isBlobContext }}>
200
+
<JSONValueInner data={value} isType={key === "$type"} isLink={key === "$link"} />
201
+
</JSONCtx.Provider>
202
</span>
203
</span>
204
);
···
220
<Show when={blob.mimeType.startsWith("image/")}>
221
<img
222
class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64"
223
+
src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${ctx.repo}&cid=${blob.ref.$link}`}
224
onLoad={() => setMediaLoaded(true)}
225
/>
226
</Show>
227
<Show when={blob.mimeType === "video/mp4"}>
228
<ErrorBoundary fallback={() => <span>Failed to load video</span>}>
229
<VideoPlayer
230
+
did={ctx.repo}
231
cid={blob.ref.$link}
232
onLoad={() => setMediaLoaded(true)}
233
/>
···
261
return rawObj;
262
};
263
264
+
const JSONArray = (props: { data: JSONType[] }) => {
265
return (
266
<For each={props.data}>
267
{(value, index) => (
···
272
}}
273
>
274
<span class="ml-[1ch] w-full">
275
+
<JSONValueInner data={value} />
276
</span>
277
</span>
278
)}
···
280
);
281
};
282
283
+
const JSONValueInner = (props: { data: JSONType; isType?: boolean; isLink?: boolean }) => {
284
const data = props.data;
285
if (typeof data === "string")
286
+
return <JSONString data={data} isType={props.isType} isLink={props.isLink} />;
287
if (typeof data === "number") return <JSONNumber data={data} />;
288
if (typeof data === "boolean") return <JSONBoolean data={data} />;
289
if (data === null) return <JSONNull />;
290
+
if (Array.isArray(data)) return <JSONArray data={data} />;
291
+
return <JSONObject data={data} />;
292
+
};
293
+
294
+
export const JSONValue = (props: { data: JSONType; repo: string; truncate?: boolean }) => {
295
+
return (
296
+
<JSONCtx.Provider value={{ repo: props.repo, truncate: props.truncate }}>
297
+
<JSONValueInner data={props.data} />
298
+
</JSONCtx.Provider>
299
+
);
300
};
301
302
export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
+4
-4
src/components/search.tsx
+4
-4
src/components/search.tsx
···
188
<label for="input" class="hidden">
189
PDS URL, AT URI, NSID, DID, or handle
190
</label>
191
-
<div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400">
192
<label
193
for="input"
194
class="iconify lucide--search text-neutral-500 dark:text-neutral-400"
···
312
src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")}
313
class="size-9 rounded-full"
314
/>
315
-
<div class="flex flex-col">
316
<Show when={actor.displayName}>
317
-
<span class="text-sm font-medium">{actor.displayName}</span>
318
</Show>
319
-
<span class="text-xs text-neutral-600 dark:text-neutral-400">
320
@{actor.handle}
321
</span>
322
</div>
···
188
<label for="input" class="hidden">
189
PDS URL, AT URI, NSID, DID, or handle
190
</label>
191
+
<div class="dark:bg-dark-100 flex items-center gap-2 rounded-lg bg-white px-2 outline-1 outline-neutral-200 focus-within:outline-[1.5px] focus-within:outline-neutral-600 dark:outline-neutral-600 dark:focus-within:outline-neutral-400">
192
<label
193
for="input"
194
class="iconify lucide--search text-neutral-500 dark:text-neutral-400"
···
312
src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")}
313
class="size-9 rounded-full"
314
/>
315
+
<div class="flex min-w-0 flex-col">
316
<Show when={actor.displayName}>
317
+
<span class="truncate text-sm font-medium">{actor.displayName}</span>
318
</Show>
319
+
<span class="truncate text-xs text-neutral-600 dark:text-neutral-400">
320
@{actor.handle}
321
</span>
322
</div>
+1
-1
src/components/text-input.tsx
+1
-1
src/components/text-input.tsx
···
25
disabled={props.disabled}
26
required={props.required}
27
class={
28
-
"dark:bg-dark-100 dark:inset-shadow-dark-200 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs select-none placeholder:text-sm focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400 " +
29
props.class
30
}
31
onInput={props.onInput}
···
25
disabled={props.disabled}
26
required={props.required}
27
class={
28
+
"dark:bg-dark-100 rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 select-none placeholder:text-sm focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400 " +
29
props.class
30
}
31
onInput={props.onInput}
+2
-2
src/layout.tsx
+2
-2
src/layout.tsx
···
118
});
119
120
return (
121
-
<div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4">
122
<MetaProvider>
123
<Show when={location.pathname !== "/"}>
124
<Meta name="robots" content="noindex, nofollow" />
···
151
<DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5">
152
<NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" />
153
<NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" />
154
-
<NavMenu href="/labels" label="Labels" icon="lucide--tags" />
155
<NavMenu href="/settings" label="Settings" icon="lucide--settings" />
156
<MenuSeparator />
157
<NavMenu
···
118
});
119
120
return (
121
+
<div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-3">
122
<MetaProvider>
123
<Show when={location.pathname !== "/"}>
124
<Meta name="robots" content="noindex, nofollow" />
···
151
<DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5">
152
<NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" />
153
<NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" />
154
+
<NavMenu href="/labels" label="Labels" icon="lucide--tag" />
155
<NavMenu href="/settings" label="Settings" icon="lucide--settings" />
156
<MenuSeparator />
157
<NavMenu
+24
src/utils/route-cache.ts
+24
src/utils/route-cache.ts
···
···
1
+
import { createStore } from "solid-js/store";
2
+
3
+
export interface CollectionCacheEntry {
4
+
records: unknown[];
5
+
cursor: string | undefined;
6
+
scrollY: number;
7
+
reverse: boolean;
8
+
}
9
+
10
+
type RouteCache = Record<string, CollectionCacheEntry>;
11
+
12
+
const [routeCache, setRouteCache] = createStore<RouteCache>({});
13
+
14
+
export const getCollectionCache = (key: string): CollectionCacheEntry | undefined => {
15
+
return routeCache[key];
16
+
};
17
+
18
+
export const setCollectionCache = (key: string, entry: CollectionCacheEntry): void => {
19
+
setRouteCache(key, entry);
20
+
};
21
+
22
+
export const clearCollectionCache = (key: string): void => {
23
+
setRouteCache(key, undefined!);
24
+
};
+4
-3
src/views/blob.tsx
+4
-3
src/views/blob.tsx
···
30
return (
31
<div class="flex flex-col items-center gap-2">
32
<Show when={blobs() || response()}>
33
-
<div class="flex w-full flex-col gap-0.5 font-mono text-xs wrap-anywhere">
34
<For each={blobs()}>
35
{(cid) => (
36
<a
37
href={`${props.pds}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${cid}`}
38
target="_blank"
39
-
class="w-fit rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
40
>
41
-
<span class="text-blue-400">{cid}</span>
42
</a>
43
)}
44
</For>
···
30
return (
31
<div class="flex flex-col items-center gap-2">
32
<Show when={blobs() || response()}>
33
+
<div class="flex w-full flex-col gap-0.5 pb-20 font-mono text-xs sm:text-sm">
34
<For each={blobs()}>
35
{(cid) => (
36
<a
37
href={`${props.pds}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${cid}`}
38
target="_blank"
39
+
class="truncate rounded px-0.5 text-left text-blue-400 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
40
+
dir="rtl"
41
>
42
+
{cid}
43
</a>
44
)}
45
</For>
+75
-21
src/views/collection.tsx
+75
-21
src/views/collection.tsx
···
2
import { Client, simpleFetchHandler } from "@atcute/client";
3
import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons";
4
import * as TID from "@atcute/tid";
5
-
import { A, useParams } from "@solidjs/router";
6
-
import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js";
7
import { createStore } from "solid-js/store";
8
import { hasUserScope } from "../auth/scope-utils";
9
import { agent } from "../auth/state";
···
17
import { isTouchDevice } from "../layout.jsx";
18
import { resolvePDS } from "../utils/api.js";
19
import { localDateFromTimestamp } from "../utils/date.js";
20
21
interface AtprotoRecord {
22
rkey: string;
···
43
44
return (
45
<span
46
-
class="relative flex w-full min-w-0 items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
47
ref={rkeyRef}
48
onmouseover={() => !isTouchDevice && setHover(true)}
49
onmouseleave={() => !isTouchDevice && setHover(false)}
50
>
51
<span class="flex items-baseline truncate">
52
-
<span class="shrink-0 text-sm text-blue-400 sm:text-base">{props.record.rkey}</span>
53
<span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl">
54
{props.record.cid}
55
</span>
···
67
<JSONValue
68
data={props.record.record.value as JSONType}
69
repo={props.record.record.uri.split("/")[2]}
70
/>
71
</span>
72
</Show>
···
84
const [reverse, setReverse] = createSignal(false);
85
const [recreate, setRecreate] = createSignal(false);
86
const [openDelete, setOpenDelete] = createSignal(false);
87
const did = params.repo;
88
let pds: string;
89
let rpc: Client;
90
91
const fetchRecords = async () => {
92
if (!pds) pds = await resolvePDS(did!);
93
if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
94
const res = await rpc.get("com.atproto.repo.listRecords", {
···
167
setCursor(undefined);
168
setOpenDelete(false);
169
setRecreate(false);
170
refetch();
171
};
172
···
211
setLastSelected(undefined);
212
setBatchDelete(!batchDelete());
213
}}
214
-
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
215
>
216
<span
217
-
class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `}
218
></span>
219
</button>
220
}
···
225
children={
226
<button
227
onclick={() => selectAll()}
228
-
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
229
>
230
-
<span class="iconify lucide--copy-check text-lg"></span>
231
</button>
232
}
233
/>
···
240
setRecreate(true);
241
setOpenDelete(true);
242
}}
243
-
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
244
>
245
-
<span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span>
246
</button>
247
}
248
/>
···
255
setRecreate(false);
256
setOpenDelete(true);
257
}}
258
-
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
259
>
260
-
<span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span>
261
</button>
262
}
263
/>
···
281
</div>
282
</Modal>
283
</Show>
284
-
<Tooltip text="Jetstream">
285
-
<A
286
-
href={`/jetstream?collections=${params.collection}&dids=${params.repo}`}
287
-
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
288
-
>
289
-
<span class="iconify lucide--radio-tower text-lg"></span>
290
-
</A>
291
-
</Tooltip>
292
<TextInput
293
name="Filter"
294
placeholder="Filter by substring"
295
onInput={(e) => setFilter(e.currentTarget.value)}
296
class="grow"
297
/>
298
</div>
299
<Show when={records.length > 1}>
300
<div class="flex items-center justify-between gap-x-2">
···
303
setReverse(!reverse());
304
setRecords([]);
305
setCursor(undefined);
306
refetch();
307
}}
308
>
···
350
</label>
351
</Show>
352
<Show when={!batchDelete()}>
353
-
<A href={`/at://${did}/${params.collection}/${record.rkey}`}>
354
<RecordLink record={record} />
355
</A>
356
</Show>
···
2
import { Client, simpleFetchHandler } from "@atcute/client";
3
import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons";
4
import * as TID from "@atcute/tid";
5
+
import { A, useBeforeLeave, useParams } from "@solidjs/router";
6
+
import {
7
+
createEffect,
8
+
createMemo,
9
+
createResource,
10
+
createSignal,
11
+
For,
12
+
onMount,
13
+
Show,
14
+
} from "solid-js";
15
import { createStore } from "solid-js/store";
16
import { hasUserScope } from "../auth/scope-utils";
17
import { agent } from "../auth/state";
···
25
import { isTouchDevice } from "../layout.jsx";
26
import { resolvePDS } from "../utils/api.js";
27
import { localDateFromTimestamp } from "../utils/date.js";
28
+
import {
29
+
clearCollectionCache,
30
+
getCollectionCache,
31
+
setCollectionCache,
32
+
} from "../utils/route-cache.js";
33
34
interface AtprotoRecord {
35
rkey: string;
···
56
57
return (
58
<span
59
+
class="relative flex w-full min-w-0 items-baseline rounded p-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
60
ref={rkeyRef}
61
onmouseover={() => !isTouchDevice && setHover(true)}
62
onmouseleave={() => !isTouchDevice && setHover(false)}
63
>
64
<span class="flex items-baseline truncate">
65
+
<span class="shrink-0 text-sm text-blue-400">{props.record.rkey}</span>
66
<span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl">
67
{props.record.cid}
68
</span>
···
80
<JSONValue
81
data={props.record.record.value as JSONType}
82
repo={props.record.record.uri.split("/")[2]}
83
+
truncate
84
/>
85
</span>
86
</Show>
···
98
const [reverse, setReverse] = createSignal(false);
99
const [recreate, setRecreate] = createSignal(false);
100
const [openDelete, setOpenDelete] = createSignal(false);
101
+
const [restoredFromCache, setRestoredFromCache] = createSignal(false);
102
const did = params.repo;
103
let pds: string;
104
let rpc: Client;
105
106
+
const cacheKey = () => `${params.pds}/${params.repo}/${params.collection}`;
107
+
108
+
onMount(() => {
109
+
const cached = getCollectionCache(cacheKey());
110
+
if (cached) {
111
+
setRecords(cached.records as AtprotoRecord[]);
112
+
setCursor(cached.cursor);
113
+
setReverse(cached.reverse);
114
+
setRestoredFromCache(true);
115
+
requestAnimationFrame(() => {
116
+
window.scrollTo(0, cached.scrollY);
117
+
});
118
+
}
119
+
});
120
+
121
+
useBeforeLeave((e) => {
122
+
const recordPathPrefix = `/at://${did}/${params.collection}/`;
123
+
const isNavigatingToRecord = typeof e.to === "string" && e.to.startsWith(recordPathPrefix);
124
+
125
+
if (isNavigatingToRecord && records.length > 0) {
126
+
setCollectionCache(cacheKey(), {
127
+
records: [...records],
128
+
cursor: cursor(),
129
+
scrollY: window.scrollY,
130
+
reverse: reverse(),
131
+
});
132
+
} else {
133
+
clearCollectionCache(cacheKey());
134
+
}
135
+
});
136
+
137
const fetchRecords = async () => {
138
+
if (restoredFromCache() && records.length > 0 && !cursor()) {
139
+
setRestoredFromCache(false);
140
+
return records;
141
+
}
142
+
if (restoredFromCache()) setRestoredFromCache(false);
143
+
144
if (!pds) pds = await resolvePDS(did!);
145
if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
146
const res = await rpc.get("com.atproto.repo.listRecords", {
···
219
setCursor(undefined);
220
setOpenDelete(false);
221
setRecreate(false);
222
+
clearCollectionCache(cacheKey());
223
refetch();
224
};
225
···
264
setLastSelected(undefined);
265
setBatchDelete(!batchDelete());
266
}}
267
+
class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
268
>
269
<span
270
+
class={`iconify ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `}
271
></span>
272
</button>
273
}
···
278
children={
279
<button
280
onclick={() => selectAll()}
281
+
class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
282
>
283
+
<span class="iconify lucide--copy-check"></span>
284
</button>
285
}
286
/>
···
293
setRecreate(true);
294
setOpenDelete(true);
295
}}
296
+
class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
297
>
298
+
<span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span>
299
</button>
300
}
301
/>
···
308
setRecreate(false);
309
setOpenDelete(true);
310
}}
311
+
class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
312
>
313
+
<span class="iconify lucide--trash-2 text-red-500 dark:text-red-400"></span>
314
</button>
315
}
316
/>
···
334
</div>
335
</Modal>
336
</Show>
337
<TextInput
338
name="Filter"
339
placeholder="Filter by substring"
340
onInput={(e) => setFilter(e.currentTarget.value)}
341
class="grow"
342
/>
343
+
<Tooltip text="Jetstream">
344
+
<A
345
+
href={`/jetstream?collections=${params.collection}&dids=${params.repo}`}
346
+
class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
347
+
>
348
+
<span class="iconify lucide--radio-tower"></span>
349
+
</A>
350
+
</Tooltip>
351
</div>
352
<Show when={records.length > 1}>
353
<div class="flex items-center justify-between gap-x-2">
···
356
setReverse(!reverse());
357
setRecords([]);
358
setCursor(undefined);
359
+
clearCollectionCache(cacheKey());
360
refetch();
361
}}
362
>
···
404
</label>
405
</Show>
406
<Show when={!batchDelete()}>
407
+
<A href={`/at://${did}/${params.collection}/${record.rkey}`} class="select-none">
408
<RecordLink record={record} />
409
</A>
410
</Show>
+1
-1
src/views/labels.tsx
+1
-1
src/views/labels.tsx
···
228
rows={2}
229
value={searchParams.uriPatterns ?? "*"}
230
placeholder="at://did:web:example.com/app.bsky.feed.post/*"
231
-
class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1.5 text-sm inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
232
/>
233
</label>
234
</div>
···
228
rows={2}
229
value={searchParams.uriPatterns ?? "*"}
230
placeholder="at://did:web:example.com/app.bsky.feed.post/*"
231
+
class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1.5 text-sm outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400"
232
/>
233
</label>
234
</div>
+11
-20
src/views/logs.tsx
+11
-20
src/views/logs.tsx
···
55
}
56
});
57
58
-
const FilterButton = (props: { icon: string; event: PlcEvent; label: string }) => {
59
const isActive = () => activePlcEvent() === props.event;
60
const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event);
61
62
return (
63
<button
64
classList={{
65
-
"flex items-center gap-1 sm:gap-1.5 rounded-lg px-3 py-2 sm:px-2 sm:py-1.5 text-base sm:text-sm transition-colors": true,
66
-
"bg-neutral-700 text-white dark:bg-neutral-200 dark:text-neutral-900": isActive(),
67
"bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600":
68
!isActive(),
69
}}
70
onclick={toggleFilter}
71
>
72
-
<span class={props.icon}></span>
73
-
<span class="hidden font-medium sm:inline">{props.label}</span>
74
</button>
75
);
76
};
···
255
<div class="iconify lucide--filter" />
256
<p class="font-medium">Filter by type</p>
257
</div>
258
-
<div class="flex flex-wrap gap-1 sm:gap-2">
259
-
<FilterButton icon="iconify lucide--at-sign" event="handle" label="Alias" />
260
-
<FilterButton icon="iconify lucide--hard-drive" event="service" label="Service" />
261
-
<FilterButton
262
-
icon="iconify lucide--shield-check"
263
-
event="verification_method"
264
-
label="Verification"
265
-
/>
266
-
<FilterButton
267
-
icon="iconify lucide--key-round"
268
-
event="rotation_key"
269
-
label="Rotation Key"
270
-
/>
271
</div>
272
</div>
273
<div class="flex items-center gap-1.5 text-sm font-medium">
274
<Show when={validLog() === true}>
275
-
<span class="iconify lucide--check-circle-2 text-green-600 dark:text-green-400"></span>
276
<span>Valid log</span>
277
</Show>
278
<Show when={validLog() === false}>
279
-
<span class="iconify lucide--x-circle text-red-500 dark:text-red-400"></span>
280
<span>Log validation failed</span>
281
</Show>
282
<Show when={validLog() === undefined}>
···
55
}
56
});
57
58
+
const FilterButton = (props: { event: PlcEvent; label: string }) => {
59
const isActive = () => activePlcEvent() === props.event;
60
const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event);
61
62
return (
63
<button
64
classList={{
65
+
"font-medium rounded-lg px-2 py-1.5 text-xs sm:text-sm transition-colors": true,
66
+
"bg-neutral-700 text-white dark:bg-neutral-300 dark:text-neutral-900": isActive(),
67
"bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600":
68
!isActive(),
69
}}
70
onclick={toggleFilter}
71
>
72
+
{props.label}
73
</button>
74
);
75
};
···
254
<div class="iconify lucide--filter" />
255
<p class="font-medium">Filter by type</p>
256
</div>
257
+
<div class="flex flex-wrap gap-1">
258
+
<FilterButton event="handle" label="Alias" />
259
+
<FilterButton event="service" label="Service" />
260
+
<FilterButton event="verification_method" label="Verification" />
261
+
<FilterButton event="rotation_key" label="Rotation Key" />
262
</div>
263
</div>
264
<div class="flex items-center gap-1.5 text-sm font-medium">
265
<Show when={validLog() === true}>
266
+
<span class="iconify lucide--check text-green-600 dark:text-green-400"></span>
267
<span>Valid log</span>
268
</Show>
269
<Show when={validLog() === false}>
270
+
<span class="iconify lucide--x text-red-500 dark:text-red-400"></span>
271
<span>Log validation failed</span>
272
</Show>
273
<Show when={validLog() === undefined}>
+37
-34
src/views/pds.tsx
+37
-34
src/views/pds.tsx
···
5
import { A, useLocation, useParams } from "@solidjs/router";
6
import { createResource, createSignal, For, Show } from "solid-js";
7
import { Button } from "../components/button";
8
-
import { CopyMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown";
9
import { Modal } from "../components/modal";
10
import { setPDS } from "../components/navbar";
11
import Tooltip from "../components/tooltip";
···
137
);
138
};
139
140
-
const Tab = (props: { tab: "repos" | "info"; label: string }) => (
141
<A
142
classList={{
143
-
"border-b-2": true,
144
-
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
145
(!!location.hash && location.hash !== `#${props.tab}`) ||
146
(!location.hash && props.tab !== "repos"),
147
}}
148
-
href={`/${params.pds}#${props.tab}`}
149
>
150
{props.label}
151
</A>
···
153
154
return (
155
<Show when={repos() || response()}>
156
-
<div class="flex w-full flex-col">
157
-
<div class="dark:shadow-dark-700 dark:bg-dark-300 mb-2 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
158
-
<div class="ml-1 flex items-center gap-3">
159
-
<Tab tab="repos" label="Repositories" />
160
-
<Tab tab="info" label="Info" />
161
-
</div>
162
-
<MenuProvider>
163
-
<DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5">
164
-
<CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" />
165
-
<NavMenu
166
-
href={`/firehose?instance=wss://${params.pds}`}
167
-
label="Firehose"
168
-
icon="lucide--radio-tower"
169
-
/>
170
-
</DropdownMenu>
171
-
</MenuProvider>
172
</div>
173
-
<div class="flex flex-col gap-1 px-2">
174
-
<Show when={!location.hash || location.hash === "#repos"}>
175
-
<div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700">
176
-
<For each={repos()}>{(repo) => <RepoCard {...repo} />}</For>
177
-
</div>
178
-
</Show>
179
<Show when={location.hash === "#info"}>
180
<Show when={version()}>
181
{(version) => (
182
-
<div class="flex items-baseline gap-x-1">
183
<span class="font-semibold">Version</span>
184
-
<span class="truncate text-sm">{version()}</span>
185
</div>
186
)}
187
</Show>
188
<Show when={serverInfos()}>
189
{(server) => (
190
<>
191
-
<div class="flex items-baseline gap-x-1">
192
<span class="font-semibold">DID</span>
193
-
<span class="truncate text-sm">{server().did}</span>
194
</div>
195
-
<Show when={server().inviteCodeRequired}>
196
<span class="font-semibold">Invite Code Required</span>
197
-
</Show>
198
<Show when={server().phoneVerificationRequired}>
199
-
<span class="font-semibold">Phone Verification Required</span>
200
</Show>
201
<Show when={server().availableUserDomains.length}>
202
<div class="flex flex-col">
···
5
import { A, useLocation, useParams } from "@solidjs/router";
6
import { createResource, createSignal, For, Show } from "solid-js";
7
import { Button } from "../components/button";
8
import { Modal } from "../components/modal";
9
import { setPDS } from "../components/navbar";
10
import Tooltip from "../components/tooltip";
···
136
);
137
};
138
139
+
const Tab = (props: { tab: "repos" | "info" | "firehose"; label: string }) => (
140
<A
141
classList={{
142
+
"border-b-2 font-medium": true,
143
+
"border-transparent dark:text-neutral-300/80 text-neutral-600 hover:border-neutral-600 dark:hover:border-neutral-300/80":
144
(!!location.hash && location.hash !== `#${props.tab}`) ||
145
(!location.hash && props.tab !== "repos"),
146
}}
147
+
href={
148
+
props.tab === "firehose" ?
149
+
`/firehose?instance=wss://${params.pds}`
150
+
: `/${params.pds}#${props.tab}`
151
+
}
152
>
153
{props.label}
154
</A>
···
156
157
return (
158
<Show when={repos() || response()}>
159
+
<div class="flex w-full flex-col px-2">
160
+
<div class="mb-3 flex gap-4 text-sm sm:text-base">
161
+
<Tab tab="repos" label="Repositories" />
162
+
<Tab tab="info" label="Info" />
163
+
<Tab tab="firehose" label="Firehose" />
164
</div>
165
+
<Show when={!location.hash || location.hash === "#repos"}>
166
+
<div class="flex flex-col divide-y-[0.5px] divide-neutral-300 pb-20 dark:divide-neutral-700">
167
+
<For each={repos()}>{(repo) => <RepoCard {...repo} />}</For>
168
+
</div>
169
+
</Show>
170
+
<div class="flex flex-col gap-2">
171
<Show when={location.hash === "#info"}>
172
<Show when={version()}>
173
{(version) => (
174
+
<div class="flex flex-col">
175
<span class="font-semibold">Version</span>
176
+
<span class="text-sm text-neutral-700 dark:text-neutral-300">{version()}</span>
177
</div>
178
)}
179
</Show>
180
<Show when={serverInfos()}>
181
{(server) => (
182
<>
183
+
<div class="flex flex-col">
184
<span class="font-semibold">DID</span>
185
+
<span class="text-sm">{server().did}</span>
186
</div>
187
+
<div class="flex items-center gap-1">
188
<span class="font-semibold">Invite Code Required</span>
189
+
<span
190
+
classList={{
191
+
"iconify lucide--check text-green-500 dark:text-green-400":
192
+
server().inviteCodeRequired === true,
193
+
"iconify lucide--x text-red-500 dark:text-red-400":
194
+
!server().inviteCodeRequired,
195
+
}}
196
+
></span>
197
+
</div>
198
<Show when={server().phoneVerificationRequired}>
199
+
<div class="flex items-center gap-1">
200
+
<span class="font-semibold">Phone Verification Required</span>
201
+
<span class="iconify lucide--check text-green-500 dark:text-green-400"></span>
202
+
</div>
203
</Show>
204
<Show when={server().availableUserDomains.length}>
205
<div class="flex flex-col">
+7
-18
src/views/record.tsx
+7
-18
src/views/record.tsx
···
363
<div class="flex items-center gap-0.5">
364
<A
365
classList={{
366
-
"border-b-2": true,
367
-
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
368
!isActive(),
369
}}
370
href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`}
···
381
return (
382
<Show when={record()} keyed>
383
<div class="flex w-full flex-col items-center">
384
-
<div class="dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
385
-
<div class="ml-1 flex items-center gap-3">
386
<RecordTab tab="record" label="Record" />
387
<RecordTab tab="schema" label="Schema" />
388
<RecordTab tab="backlinks" label="Backlinks" />
···
490
<Show when={location.hash === "#info"}>
491
<div class="flex w-full flex-col gap-2 px-2 text-sm">
492
<div>
493
-
<div class="flex items-center gap-1">
494
-
<span class="iconify lucide--at-sign"></span>
495
-
<p class="font-semibold">AT URI</p>
496
-
</div>
497
<div class="truncate text-xs">{record()?.uri}</div>
498
</div>
499
<Show when={record()?.cid}>
500
<div>
501
-
<div class="flex items-center gap-1">
502
-
<span class="iconify lucide--box"></span>
503
-
<p class="font-semibold">CID</p>
504
-
</div>
505
<div class="truncate text-left text-xs" dir="rtl">
506
{record()?.cid}
507
</div>
···
509
</Show>
510
<div>
511
<div class="flex items-center gap-1">
512
-
<span class="iconify lucide--lock-keyhole"></span>
513
<p class="font-semibold">Record verification</p>
514
<span
515
classList={{
···
526
</div>
527
<div>
528
<div class="flex items-center gap-1">
529
-
<span class="iconify lucide--file-check"></span>
530
<p class="font-semibold">Schema validation</p>
531
<span
532
classList={{
···
556
</div>
557
<Show when={lexiconUri()}>
558
<div>
559
-
<div class="flex items-center gap-1">
560
-
<span class="iconify lucide--scroll-text"></span>
561
-
<p class="font-semibold">Lexicon schema</p>
562
-
</div>
563
<div class="truncate text-xs">
564
<A
565
href={`/${lexiconUri()}`}
···
363
<div class="flex items-center gap-0.5">
364
<A
365
classList={{
366
+
"border-b-2 font-medium": true,
367
+
"border-transparent text-neutral-600 dark:text-neutral-300/80 hover:border-neutral-600 dark:hover:border-neutral-300/80":
368
!isActive(),
369
}}
370
href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`}
···
381
return (
382
<Show when={record()} keyed>
383
<div class="flex w-full flex-col items-center">
384
+
<div class="mb-3 flex w-full justify-between px-2 text-sm sm:text-base">
385
+
<div class="flex items-center gap-4">
386
<RecordTab tab="record" label="Record" />
387
<RecordTab tab="schema" label="Schema" />
388
<RecordTab tab="backlinks" label="Backlinks" />
···
490
<Show when={location.hash === "#info"}>
491
<div class="flex w-full flex-col gap-2 px-2 text-sm">
492
<div>
493
+
<p class="font-semibold">AT URI</p>
494
<div class="truncate text-xs">{record()?.uri}</div>
495
</div>
496
<Show when={record()?.cid}>
497
<div>
498
+
<p class="font-semibold">CID</p>
499
<div class="truncate text-left text-xs" dir="rtl">
500
{record()?.cid}
501
</div>
···
503
</Show>
504
<div>
505
<div class="flex items-center gap-1">
506
<p class="font-semibold">Record verification</p>
507
<span
508
classList={{
···
519
</div>
520
<div>
521
<div class="flex items-center gap-1">
522
<p class="font-semibold">Schema validation</p>
523
<span
524
classList={{
···
548
</div>
549
<Show when={lexiconUri()}>
550
<div>
551
+
<p class="font-semibold">Lexicon schema</p>
552
<div class="truncate text-xs">
553
<A
554
href={`/${lexiconUri()}`}
+74
-86
src/views/repo.tsx
+74
-86
src/views/repo.tsx
···
88
return (
89
<A
90
classList={{
91
-
"border-b-2": true,
92
-
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": !isActive(),
93
}}
94
href={`/at://${params.repo}#${props.tab}`}
95
>
···
275
return (
276
<Show when={repo()}>
277
<div class="flex w-full flex-col gap-3 wrap-break-word">
278
-
<div class="dark:shadow-dark-700 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
279
-
<div class="ml-1 flex items-center gap-2 text-xs sm:gap-4 sm:text-sm">
280
<Show when={!error()}>
281
<RepoTab tab="collections" label="Collections" />
282
</Show>
···
289
</Show>
290
<RepoTab tab="backlinks" label="Backlinks" />
291
</div>
292
-
<div class="flex gap-0.5">
293
<Show when={error() && error() !== "Missing PDS"}>
294
<div class="flex items-center gap-1 text-red-500 dark:text-red-400">
295
<span class="iconify lucide--alert-triangle"></span>
296
<span>{error()}</span>
297
</div>
298
</Show>
299
-
<Show when={!error() && (!location.hash || location.hash.startsWith("#collections"))}>
300
-
<Tooltip text="Filter collections">
301
-
<button
302
-
class="flex items-center rounded-sm p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
303
-
onClick={() => setShowFilter(!showFilter())}
304
-
>
305
-
<span class="iconify lucide--filter"></span>
306
-
</button>
307
-
</Tooltip>
308
-
</Show>
309
<MenuProvider>
310
<DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5">
311
<CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" />
312
<NavMenu
313
href={`/jetstream?dids=${params.repo}`}
···
323
</Show>
324
<Show when={error()?.length === 0 || error() === undefined}>
325
<ActionMenu
326
-
label="Export Repo"
327
icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"}
328
onClick={() => downloadRepo()}
329
/>
···
336
: `https://${did.split("did:web:")[1]}/.well-known/did.json`
337
}
338
newTab
339
-
label="DID Document"
340
icon="lucide--external-link"
341
/>
342
<Show when={did.startsWith("did:plc")}>
343
<NavMenu
344
href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`}
345
newTab
346
-
label="Audit Log"
347
icon="lucide--external-link"
348
/>
349
</Show>
···
486
<div class="flex flex-col gap-3 wrap-anywhere">
487
{/* ID Section */}
488
<div>
489
-
<div class="flex items-center gap-1">
490
-
<div class="iconify lucide--id-card" />
491
-
<p class="font-semibold">ID</p>
492
</div>
493
-
<div class="text-sm">{didDocument().id}</div>
494
</div>
495
496
{/* Aliases Section */}
497
<div>
498
-
<div class="flex items-center gap-1">
499
-
<div class="iconify lucide--at-sign" />
500
-
<p class="font-semibold">Aliases</p>
501
-
</div>
502
-
<div class="flex flex-col gap-0.5">
503
-
<For each={didDocument().alsoKnownAs}>
504
-
{(alias) => (
505
-
<div class="flex items-center gap-1 text-sm">
506
-
<span>{alias}</span>
507
-
<Show when={alias.startsWith("at://")}>
508
-
<Tooltip
509
-
text={
510
-
validHandles[alias] === true ? "Valid handle"
511
-
: validHandles[alias] === undefined ?
512
-
"Validating"
513
-
: "Invalid handle"
514
-
}
515
-
>
516
-
<span
517
-
classList={{
518
-
"iconify lucide--circle-check text-green-600 dark:text-green-400":
519
-
validHandles[alias] === true,
520
-
"iconify lucide--circle-x text-red-500 dark:text-red-400":
521
-
validHandles[alias] === false,
522
-
"iconify lucide--loader-circle animate-spin":
523
-
validHandles[alias] === undefined,
524
-
}}
525
-
></span>
526
-
</Tooltip>
527
-
</Show>
528
-
</div>
529
-
)}
530
-
</For>
531
-
</div>
532
</div>
533
534
{/* Services Section */}
535
<div>
536
-
<div class="flex items-center gap-1">
537
-
<div class="iconify lucide--hard-drive" />
538
-
<p class="font-semibold">Services</p>
539
-
</div>
540
-
<div class="flex flex-col gap-0.5">
541
<For each={didDocument().service}>
542
{(service) => (
543
-
<div class="text-sm">
544
-
<div class="font-medium text-neutral-700 dark:text-neutral-300">
545
-
#{service.id.split("#")[1]}
546
-
</div>
547
<a
548
-
class="underline hover:text-blue-400"
549
href={service.serviceEndpoint.toString()}
550
target="_blank"
551
rel="noopener"
···
560
561
{/* Verification Methods Section */}
562
<div>
563
-
<div class="flex items-center gap-1">
564
-
<div class="iconify lucide--shield-check" />
565
-
<p class="font-semibold">Verification Methods</p>
566
-
</div>
567
-
<div class="flex flex-col gap-0.5">
568
<For each={didDocument().verificationMethod}>
569
{(verif) => (
570
<Show when={verif.publicKeyMultibase}>
571
{(key) => (
572
-
<div class="text-sm">
573
-
<div class="flex items-baseline gap-1">
574
-
<span class="font-medium text-neutral-700 dark:text-neutral-300">
575
-
#{verif.id.split("#")[1]}
576
-
</span>
577
-
<span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300">
578
-
{detectKeyType(key())}
579
-
</span>
580
</div>
581
<div class="font-mono break-all">{key()}</div>
582
</div>
583
)}
···
590
{/* Rotation Keys Section */}
591
<Show when={rotationKeys().length > 0}>
592
<div>
593
-
<div class="flex items-center gap-1">
594
-
<div class="iconify lucide--key-round" />
595
-
<p class="font-semibold">Rotation Keys</p>
596
-
</div>
597
-
<div class="flex flex-col gap-0.5">
598
<For each={rotationKeys()}>
599
{(key) => (
600
-
<div class="text-sm">
601
-
<span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300">
602
{detectDidKeyType(key)}
603
</span>
604
<div class="font-mono break-all">{key.replace("did:key:", "")}</div>
605
</div>
606
)}
···
88
return (
89
<A
90
classList={{
91
+
"border-b-2 font-medium": true,
92
+
"border-transparent text-neutral-600 dark:text-neutral-300/80 hover:border-neutral-600 dark:hover:border-neutral-300/80":
93
+
!isActive(),
94
}}
95
href={`/at://${params.repo}#${props.tab}`}
96
>
···
276
return (
277
<Show when={repo()}>
278
<div class="flex w-full flex-col gap-3 wrap-break-word">
279
+
<div class="flex justify-between px-2 text-sm sm:text-base">
280
+
<div class="flex items-center gap-3 sm:gap-4">
281
<Show when={!error()}>
282
<RepoTab tab="collections" label="Collections" />
283
</Show>
···
290
</Show>
291
<RepoTab tab="backlinks" label="Backlinks" />
292
</div>
293
+
<div class="flex gap-1">
294
<Show when={error() && error() !== "Missing PDS"}>
295
<div class="flex items-center gap-1 text-red-500 dark:text-red-400">
296
<span class="iconify lucide--alert-triangle"></span>
297
<span>{error()}</span>
298
</div>
299
</Show>
300
<MenuProvider>
301
<DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5">
302
+
<Show
303
+
when={!error() && (!location.hash || location.hash.startsWith("#collections"))}
304
+
>
305
+
<ActionMenu
306
+
label="Filter collections"
307
+
icon="lucide--filter"
308
+
onClick={() => setShowFilter(!showFilter())}
309
+
/>
310
+
</Show>
311
<CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" />
312
<NavMenu
313
href={`/jetstream?dids=${params.repo}`}
···
323
</Show>
324
<Show when={error()?.length === 0 || error() === undefined}>
325
<ActionMenu
326
+
label="Export repo"
327
icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"}
328
onClick={() => downloadRepo()}
329
/>
···
336
: `https://${did.split("did:web:")[1]}/.well-known/did.json`
337
}
338
newTab
339
+
label="DID document"
340
icon="lucide--external-link"
341
/>
342
<Show when={did.startsWith("did:plc")}>
343
<NavMenu
344
href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`}
345
newTab
346
+
label="Audit log"
347
icon="lucide--external-link"
348
/>
349
</Show>
···
486
<div class="flex flex-col gap-3 wrap-anywhere">
487
{/* ID Section */}
488
<div>
489
+
<div class="font-semibold">DID</div>
490
+
<div class="text-sm text-neutral-700 dark:text-neutral-300">
491
+
{didDocument().id}
492
</div>
493
</div>
494
495
{/* Aliases Section */}
496
<div>
497
+
<p class="font-semibold">Aliases</p>
498
+
<For each={didDocument().alsoKnownAs}>
499
+
{(alias) => (
500
+
<div class="flex items-center gap-1 text-sm text-neutral-700 dark:text-neutral-300">
501
+
<span>{alias}</span>
502
+
<Show when={alias.startsWith("at://")}>
503
+
<Tooltip
504
+
text={
505
+
validHandles[alias] === true ? "Valid handle"
506
+
: validHandles[alias] === undefined ?
507
+
"Validating"
508
+
: "Invalid handle"
509
+
}
510
+
>
511
+
<span
512
+
classList={{
513
+
"iconify lucide--check text-green-600 dark:text-green-400":
514
+
validHandles[alias] === true,
515
+
"iconify lucide--x text-red-500 dark:text-red-400":
516
+
validHandles[alias] === false,
517
+
"iconify lucide--loader-circle animate-spin":
518
+
validHandles[alias] === undefined,
519
+
}}
520
+
></span>
521
+
</Tooltip>
522
+
</Show>
523
+
</div>
524
+
)}
525
+
</For>
526
</div>
527
528
{/* Services Section */}
529
<div>
530
+
<p class="font-semibold">Services</p>
531
+
<div class="flex flex-col gap-1">
532
<For each={didDocument().service}>
533
{(service) => (
534
+
<div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300">
535
+
<span class="iconify lucide--hash"></span>
536
+
<span>{service.id.split("#")[1]}</span>
537
+
<span></span>
538
<a
539
+
class="w-fit underline hover:text-blue-400"
540
href={service.serviceEndpoint.toString()}
541
target="_blank"
542
rel="noopener"
···
551
552
{/* Verification Methods Section */}
553
<div>
554
+
<p class="font-semibold">Verification Methods</p>
555
+
<div class="flex flex-col gap-1">
556
<For each={didDocument().verificationMethod}>
557
{(verif) => (
558
<Show when={verif.publicKeyMultibase}>
559
{(key) => (
560
+
<div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300">
561
+
<span class="iconify lucide--hash"></span>
562
+
<div class="flex items-center gap-2">
563
+
<span>{verif.id.split("#")[1]}</span>
564
+
<div class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400">
565
+
<span class="iconify lucide--key-round"></span>
566
+
<span>{detectKeyType(key())}</span>
567
+
</div>
568
</div>
569
+
<span></span>
570
<div class="font-mono break-all">{key()}</div>
571
</div>
572
)}
···
579
{/* Rotation Keys Section */}
580
<Show when={rotationKeys().length > 0}>
581
<div>
582
+
<p class="font-semibold">Rotation Keys</p>
583
+
<div class="flex flex-col gap-1">
584
<For each={rotationKeys()}>
585
{(key) => (
586
+
<div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300">
587
+
<span class="iconify lucide--key-round text-neutral-500 dark:text-neutral-400"></span>
588
+
<span class="text-neutral-500 dark:text-neutral-400">
589
{detectDidKeyType(key)}
590
</span>
591
+
<span></span>
592
<div class="font-mono break-all">{key.replace("did:key:", "")}</div>
593
</div>
594
)}
+8
-8
src/views/stream.tsx
+8
-8
src/views/stream.tsx
···
143
144
return (
145
<div class="flex w-full flex-col items-center">
146
-
<div class="flex gap-2 text-sm">
147
<A
148
-
class="flex items-center gap-1 border-b-2 p-1"
149
-
inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600"
150
href="/jetstream"
151
>
152
Jetstream
153
</A>
154
<A
155
-
class="flex items-center gap-1 border-b-2 p-1"
156
-
inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600"
157
href="/firehose"
158
>
159
Firehose
160
</A>
161
</div>
162
<StickyOverlay>
163
-
<form ref={formRef} class="flex w-full flex-col gap-1 text-sm">
164
<Show when={!connected()}>
165
<label class="flex items-center justify-end gap-x-1">
166
<span class="min-w-20">Instance</span>
···
183
spellcheck={false}
184
placeholder="Comma-separated list of collections"
185
value={searchParams.collections ?? ""}
186
-
class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
187
/>
188
</label>
189
</Show>
···
195
spellcheck={false}
196
placeholder="Comma-separated list of DIDs"
197
value={searchParams.dids ?? ""}
198
-
class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
199
/>
200
</label>
201
</Show>
···
143
144
return (
145
<div class="flex w-full flex-col items-center">
146
+
<div class="mb-1 flex gap-4 font-medium">
147
<A
148
+
class="flex items-center gap-1 border-b-2"
149
+
inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600"
150
href="/jetstream"
151
>
152
Jetstream
153
</A>
154
<A
155
+
class="flex items-center gap-1 border-b-2"
156
+
inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600"
157
href="/firehose"
158
>
159
Firehose
160
</A>
161
</div>
162
<StickyOverlay>
163
+
<form ref={formRef} class="flex w-full flex-col gap-1.5 text-sm">
164
<Show when={!connected()}>
165
<label class="flex items-center justify-end gap-x-1">
166
<span class="min-w-20">Instance</span>
···
183
spellcheck={false}
184
placeholder="Comma-separated list of collections"
185
value={searchParams.collections ?? ""}
186
+
class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400"
187
/>
188
</label>
189
</Show>
···
195
spellcheck={false}
196
placeholder="Comma-separated list of DIDs"
197
value={searchParams.dids ?? ""}
198
+
class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400"
199
/>
200
</label>
201
</Show>