tangled
alpha
login
or
join now
futur.blue
/
pdsls
forked from
pds.ls/pdsls
0
fork
atom
this repo has no description
0
fork
atom
overview
issues
pulls
pipelines
done
handle.invalid
4 months ago
855ab79f
bc07dfe1
verified
This commit was signed with the committer's
known signature
.
handle.invalid
SSH Key Fingerprint:
SHA256:mBrT4x0JdzLpbVR95g1hjI1aaErfC02kmLRkPXwsYCk=
+275
-150
6 changed files
expand all
collapse all
unified
split
src
components
button.tsx
dropdown.tsx
layout.tsx
views
labels.tsx
record.tsx
repo.tsx
+4
-1
src/components/button.tsx
···
1
1
import { JSX } from "solid-js";
2
2
3
3
export interface ButtonProps {
4
4
+
type?: "button" | "submit" | "reset" | "menu" | undefined;
5
5
+
disabled?: boolean;
4
6
class?: string;
5
7
classList?: Record<string, boolean | undefined>;
6
8
onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
···
10
12
export const Button = (props: ButtonProps) => {
11
13
return (
12
14
<button
13
13
-
type="button"
15
15
+
type={props.type ?? "button"}
16
16
+
disabled={props.disabled ?? false}
14
17
class={
15
18
props.class ??
16
19
"dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
+4
src/components/dropdown.tsx
···
89
89
);
90
90
};
91
91
92
92
+
export const MenuSeparator = () => {
93
93
+
return <div class="my-1 h-[0.5px] bg-neutral-300 dark:bg-neutral-600" />;
94
94
+
};
95
95
+
92
96
export const DropdownMenu = (props: {
93
97
icon: string;
94
98
buttonClass?: string;
+3
-2
src/layout.tsx
···
4
4
import { createEffect, ErrorBoundary, onMount, Show, Suspense } from "solid-js";
5
5
import { AccountManager } from "./components/account.jsx";
6
6
import { RecordEditor } from "./components/create.jsx";
7
7
-
import { DropdownMenu, MenuProvider, NavMenu } from "./components/dropdown.jsx";
7
7
+
import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx";
8
8
import { agent } from "./components/login.jsx";
9
9
import { NavBar } from "./components/navbar.jsx";
10
10
import { NotificationContainer } from "./components/notification.jsx";
···
141
141
<DropdownMenu
142
142
icon="lucide--menu text-xl"
143
143
buttonClass="rounded-lg p-1"
144
144
-
menuClass="top-8 p-3 text-sm"
144
144
+
menuClass="top-11 p-3 text-sm"
145
145
>
146
146
<NavMenu href="/jetstream" label="Jetstream" />
147
147
<NavMenu href="/firehose" label="Firehose" />
148
148
<NavMenu href="/labels" label="Labels" />
149
149
<NavMenu href="/settings" label="Settings" />
150
150
+
<MenuSeparator />
150
151
<NavMenu
151
152
href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz"
152
153
label="Bluesky"
+239
-139
src/views/labels.tsx
···
1
1
import { ComAtprotoLabelDefs } from "@atcute/atproto";
2
2
import { Client, CredentialManager } from "@atcute/client";
3
3
+
import { isAtprotoDid } from "@atcute/identity";
4
4
+
import { Handle } from "@atcute/lexicons";
3
5
import { A, useSearchParams } from "@solidjs/router";
4
4
-
import { createSignal, For, onMount, Show } from "solid-js";
6
6
+
import { createMemo, createSignal, For, onMount, Show } from "solid-js";
5
7
import { Button } from "../components/button.jsx";
6
8
import { StickyOverlay } from "../components/sticky.jsx";
7
9
import { TextInput } from "../components/text-input.jsx";
8
8
-
import { labelerCache, resolvePDS } from "../utils/api.js";
10
10
+
import { labelerCache, resolveHandle, resolvePDS } from "../utils/api.js";
9
11
import { localDateFromTimestamp } from "../utils/date.js";
10
12
13
13
+
const LABELS_PER_PAGE = 50;
14
14
+
15
15
+
const LabelCard = (props: { label: ComAtprotoLabelDefs.Label }) => {
16
16
+
const label = props.label;
17
17
+
18
18
+
return (
19
19
+
<div class="flex flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 dark:border-neutral-700 dark:bg-neutral-800">
20
20
+
<div class="flex flex-wrap items-center gap-x-2 gap-y-2">
21
21
+
<div class="inline-flex items-center gap-x-1 rounded-full bg-neutral-200 px-2 py-0.5 text-sm font-medium text-neutral-800 dark:bg-neutral-700 dark:text-neutral-200">
22
22
+
<span class="iconify lucide--tag shrink-0" />
23
23
+
{label.val}
24
24
+
</div>
25
25
+
<Show when={label.neg}>
26
26
+
<div class="inline-flex items-center gap-x-1 rounded-full border border-orange-400 bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 dark:border-orange-600 dark:bg-orange-900/30 dark:text-orange-400">
27
27
+
<span class="iconify lucide--minus shrink-0 text-sm" />
28
28
+
<span>Negated</span>
29
29
+
</div>
30
30
+
</Show>
31
31
+
<div class="flex flex-wrap gap-3 text-xs text-neutral-600 dark:text-neutral-400">
32
32
+
<div class="flex items-center gap-x-1">
33
33
+
<span class="iconify lucide--calendar shrink-0" />
34
34
+
<span>{localDateFromTimestamp(new Date(label.cts).getTime())}</span>
35
35
+
</div>
36
36
+
<Show when={label.exp}>
37
37
+
{(exp) => (
38
38
+
<div class="flex items-center gap-x-1">
39
39
+
<span class="iconify lucide--clock-fading shrink-0" />
40
40
+
<span>e{localDateFromTimestamp(new Date(exp()).getTime())}</span>
41
41
+
</div>
42
42
+
)}
43
43
+
</Show>
44
44
+
</div>
45
45
+
</div>
46
46
+
47
47
+
<div class="flex flex-col gap-y-0.5">
48
48
+
<div class="text-xs font-medium tracking-wide text-neutral-500 uppercase dark:text-neutral-400">
49
49
+
URI
50
50
+
</div>
51
51
+
<A
52
52
+
href={`/at://${label.uri.replace("at://", "")}`}
53
53
+
class="text-sm break-all text-blue-600 hover:underline dark:text-blue-400"
54
54
+
>
55
55
+
{label.uri}
56
56
+
</A>
57
57
+
</div>
58
58
+
59
59
+
<Show when={label.cid}>
60
60
+
<div class="flex flex-col gap-y-0.5">
61
61
+
<div class="text-xs font-medium tracking-wide text-neutral-500 uppercase dark:text-neutral-400">
62
62
+
CID
63
63
+
</div>
64
64
+
<div class="text-sm break-all text-neutral-700 dark:text-neutral-300">{label.cid}</div>
65
65
+
</div>
66
66
+
</Show>
67
67
+
</div>
68
68
+
);
69
69
+
};
70
70
+
11
71
export const LabelView = () => {
12
72
const [searchParams, setSearchParams] = useSearchParams();
13
73
const [cursor, setCursor] = createSignal<string>();
14
74
const [labels, setLabels] = createSignal<ComAtprotoLabelDefs.Label[]>([]);
15
15
-
const [filter, setFilter] = createSignal<string>();
16
16
-
const [labelCount, setLabelCount] = createSignal(0);
75
75
+
const [filter, setFilter] = createSignal("");
17
76
const [loading, setLoading] = createSignal(false);
18
18
-
let rpc: Client;
77
77
+
const [error, setError] = createSignal<string>();
78
78
+
const [didInput, setDidInput] = createSignal(searchParams.did ?? "");
79
79
+
80
80
+
let rpc: Client | undefined;
19
81
let formRef!: HTMLFormElement;
20
82
83
83
+
const filteredLabels = createMemo(() => {
84
84
+
const filterValue = filter().trim().toLowerCase();
85
85
+
if (!filterValue) return labels();
86
86
+
return labels().filter((label) => label.val.toLowerCase().includes(filterValue));
87
87
+
});
88
88
+
89
89
+
const hasSearched = createMemo(() => Boolean(searchParams.uriPatterns));
90
90
+
21
91
onMount(async () => {
22
22
-
const formData = new FormData();
23
23
-
if (searchParams.did) formData.append("did", searchParams.did.toString());
24
24
-
if (searchParams.did) fetchLabels(formData);
92
92
+
if (searchParams.did && searchParams.uriPatterns) {
93
93
+
const formData = new FormData();
94
94
+
formData.append("did", searchParams.did.toString());
95
95
+
formData.append("uriPatterns", searchParams.uriPatterns.toString());
96
96
+
await fetchLabels(formData);
97
97
+
}
25
98
});
26
99
27
100
const fetchLabels = async (formData: FormData, reset?: boolean) => {
101
101
+
let did = formData.get("did")?.toString()?.trim();
102
102
+
const uriPatterns = formData.get("uriPatterns")?.toString()?.trim();
103
103
+
104
104
+
if (!did || !uriPatterns) {
105
105
+
setError("Please provide both DID and URI patterns");
106
106
+
return;
107
107
+
}
108
108
+
28
109
if (reset) {
29
110
setLabels([]);
30
111
setCursor(undefined);
112
112
+
setError(undefined);
31
113
}
32
114
33
33
-
const did = formData.get("did")?.toString();
34
34
-
if (!did) return;
35
35
-
await resolvePDS(did);
36
36
-
rpc = new Client({
37
37
-
handler: new CredentialManager({ service: labelerCache[did] }),
38
38
-
});
115
115
+
try {
116
116
+
setLoading(true);
117
117
+
setError(undefined);
39
118
40
40
-
const uriPatterns = formData.get("uriPatterns")?.toString();
41
41
-
if (!uriPatterns) return;
119
119
+
if (!isAtprotoDid(did)) did = await resolveHandle(did as Handle);
120
120
+
await resolvePDS(did);
121
121
+
if (!labelerCache[did]) throw new Error("Repository is not a labeler");
122
122
+
rpc = new Client({
123
123
+
handler: new CredentialManager({ service: labelerCache[did] }),
124
124
+
});
42
125
43
43
-
setSearchParams({
44
44
-
did: formData.get("did")?.toString(),
45
45
-
uriPatterns: formData.get("uriPatterns")?.toString(),
46
46
-
});
126
126
+
setSearchParams({ did, uriPatterns });
127
127
+
setDidInput(did);
128
128
+
129
129
+
const res = await rpc.get("com.atproto.label.queryLabels", {
130
130
+
params: {
131
131
+
uriPatterns: uriPatterns.split(",").map((p) => p.trim()),
132
132
+
sources: [did as `did:${string}:${string}`],
133
133
+
cursor: cursor(),
134
134
+
},
135
135
+
});
47
136
48
48
-
setLoading(true);
49
49
-
const res = await rpc.get("com.atproto.label.queryLabels", {
50
50
-
params: {
51
51
-
uriPatterns: uriPatterns.toString().trim().split(","),
52
52
-
sources: [did as `did:${string}:${string}`],
53
53
-
cursor: cursor(),
54
54
-
},
55
55
-
});
56
56
-
setLoading(false);
57
57
-
if (!res.ok) throw new Error(res.data.error);
58
58
-
setCursor(res.data.labels.length < 50 ? undefined : res.data.cursor);
59
59
-
setLabels(labels().concat(res.data.labels) ?? res.data.labels);
60
60
-
return res.data.labels;
137
137
+
if (!res.ok) throw new Error(res.data.error || "Failed to fetch labels");
138
138
+
139
139
+
const newLabels = res.data.labels || [];
140
140
+
setCursor(newLabels.length < LABELS_PER_PAGE ? undefined : res.data.cursor);
141
141
+
setLabels(reset ? newLabels : [...labels(), ...newLabels]);
142
142
+
} catch (err) {
143
143
+
setError(err instanceof Error ? err.message : "An error occurred");
144
144
+
console.error("Failed to fetch labels:", err);
145
145
+
} finally {
146
146
+
setLoading(false);
147
147
+
}
61
148
};
62
149
63
63
-
const filterLabels = () => {
64
64
-
const newFilter = labels().filter((label) => (filter() ? filter() === label.val : true));
65
65
-
setLabelCount(newFilter.length);
66
66
-
return newFilter;
150
150
+
const handleSearch = () => {
151
151
+
fetchLabels(new FormData(formRef), true);
152
152
+
};
153
153
+
154
154
+
const handleLoadMore = () => {
155
155
+
fetchLabels(new FormData(formRef));
67
156
};
68
157
69
158
return (
70
159
<div class="flex w-full flex-col items-center">
71
71
-
<form ref={formRef} class="flex w-full flex-col items-center gap-y-1 px-2">
72
72
-
<label class="flex w-full items-center gap-x-2 px-1">
73
73
-
<span class="">DID</span>
74
74
-
<TextInput name="did" value={searchParams.did ?? ""} class="grow" />
75
75
-
</label>
76
76
-
<label for="uriPatterns" class="ml-2 w-full text-sm">
77
77
-
URI Patterns (comma-separated)
78
78
-
</label>
79
79
-
<div class="flex w-full items-center gap-x-1 px-1">
80
80
-
<textarea
81
81
-
id="uriPatterns"
82
82
-
name="uriPatterns"
83
83
-
spellcheck={false}
84
84
-
rows={2}
85
85
-
value={searchParams.uriPatterns ?? "*"}
86
86
-
class="dark:bg-dark-100 dark:shadow-dark-700 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 text-sm shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
87
87
-
/>
88
88
-
<div class="flex justify-center">
89
89
-
<Show when={!loading()}>
90
90
-
<button
91
91
-
type="button"
92
92
-
onClick={() => fetchLabels(new FormData(formRef), true)}
93
93
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
94
94
-
>
95
95
-
<span class="iconify lucide--search text-lg"></span>
96
96
-
</button>
97
97
-
</Show>
98
98
-
<Show when={loading()}>
99
99
-
<div class="m-1 flex items-center">
100
100
-
<span class="iconify lucide--loader-circle animate-spin text-lg"></span>
101
101
-
</div>
102
102
-
</Show>
160
160
+
<form
161
161
+
ref={formRef}
162
162
+
class="flex w-full max-w-3xl flex-col gap-y-2 px-3 py-2"
163
163
+
onSubmit={(e) => {
164
164
+
e.preventDefault();
165
165
+
handleSearch();
166
166
+
}}
167
167
+
>
168
168
+
<div class="flex flex-col gap-y-1.5">
169
169
+
<label class="flex w-full flex-col gap-y-1">
170
170
+
<span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">
171
171
+
Labeler DID/Handle
172
172
+
</span>
173
173
+
<TextInput
174
174
+
name="did"
175
175
+
value={didInput()}
176
176
+
onInput={(e) => setDidInput(e.currentTarget.value)}
177
177
+
placeholder="did:plc:..."
178
178
+
class="w-full"
179
179
+
/>
180
180
+
</label>
181
181
+
182
182
+
<label class="flex w-full flex-col gap-y-1">
183
183
+
<span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">
184
184
+
URI Patterns (comma-separated)
185
185
+
</span>
186
186
+
<textarea
187
187
+
id="uriPatterns"
188
188
+
name="uriPatterns"
189
189
+
spellcheck={false}
190
190
+
rows={2}
191
191
+
value={searchParams.uriPatterns ?? "*"}
192
192
+
placeholder="at://did:web:example.com/app.bsky.feed.post/*"
193
193
+
class="dark:bg-dark-100 dark:shadow-dark-700 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1.5 text-sm shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
194
194
+
/>
195
195
+
</label>
196
196
+
</div>
197
197
+
198
198
+
<Button
199
199
+
type="submit"
200
200
+
disabled={loading()}
201
201
+
class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-fit items-center justify-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
202
202
+
>
203
203
+
<span class="iconify lucide--search" />
204
204
+
<span>Search Labels</span>
205
205
+
</Button>
206
206
+
207
207
+
<Show when={error()}>
208
208
+
<div class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300">
209
209
+
{error()}
103
210
</div>
104
104
-
</div>
211
211
+
</Show>
105
212
</form>
106
106
-
<StickyOverlay>
107
107
-
<TextInput
108
108
-
placeholder="Filter by label"
109
109
-
name="filter"
110
110
-
onInput={(e) => setFilter(e.currentTarget.value)}
111
111
-
class="w-full text-sm"
112
112
-
/>
113
113
-
<div class="flex items-center gap-x-2">
114
114
-
<Show when={labelCount() && labels().length}>
115
115
-
<div>
116
116
-
<span>
117
117
-
{labelCount()} label{labelCount() > 1 ? "s" : ""}
118
118
-
</span>
119
119
-
</div>
120
120
-
</Show>
121
121
-
<Show when={cursor()}>
122
122
-
<div class="flex h-8 w-22 items-center justify-center text-nowrap">
123
123
-
<Show when={!loading()}>
124
124
-
<Button onClick={() => fetchLabels(new FormData(formRef))}>Load More</Button>
213
213
+
214
214
+
<Show when={hasSearched()}>
215
215
+
<StickyOverlay>
216
216
+
<div class="flex w-full items-center gap-x-2">
217
217
+
<TextInput
218
218
+
placeholder="Filter by label value"
219
219
+
name="filter"
220
220
+
value={filter()}
221
221
+
onInput={(e) => setFilter(e.currentTarget.value)}
222
222
+
class="min-w-0 grow text-sm"
223
223
+
/>
224
224
+
<div class="flex shrink-0 items-center gap-x-2 text-sm">
225
225
+
<Show when={labels().length > 0}>
226
226
+
<span class="whitespace-nowrap text-neutral-600 dark:text-neutral-400">
227
227
+
{filteredLabels().length}/{labels().length}
228
228
+
</span>
125
229
</Show>
126
126
-
<Show when={loading()}>
127
127
-
<div class="iconify lucide--loader-circle animate-spin text-xl" />
230
230
+
231
231
+
<Show when={cursor()}>
232
232
+
<Button
233
233
+
onClick={handleLoadMore}
234
234
+
disabled={loading()}
235
235
+
class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-20 items-center justify-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
236
236
+
>
237
237
+
<Show
238
238
+
when={!loading()}
239
239
+
fallback={<span class="iconify lucide--loader-circle animate-spin" />}
240
240
+
>
241
241
+
Load More
242
242
+
</Show>
243
243
+
</Button>
128
244
</Show>
129
245
</div>
246
246
+
</div>
247
247
+
</StickyOverlay>
248
248
+
249
249
+
<div class="w-full max-w-3xl px-3 py-2">
250
250
+
<Show when={loading() && labels().length === 0}>
251
251
+
<div class="flex flex-col items-center justify-center py-12 text-center">
252
252
+
<span class="iconify lucide--loader-circle mb-3 animate-spin text-4xl text-neutral-400" />
253
253
+
<p class="text-sm text-neutral-600 dark:text-neutral-400">Loading labels...</p>
254
254
+
</div>
130
255
</Show>
131
131
-
</div>
132
132
-
</StickyOverlay>
133
133
-
<Show when={labels().length}>
134
134
-
<div class="flex flex-col gap-2 divide-y-[0.5px] divide-neutral-400 text-sm wrap-anywhere whitespace-pre-wrap dark:divide-neutral-600">
135
135
-
<For each={filterLabels()}>
136
136
-
{(label) => (
137
137
-
<div class="flex items-center justify-between gap-2 pb-2">
138
138
-
<div class="flex flex-col">
139
139
-
<div class="flex items-center gap-x-2">
140
140
-
<div class="min-w-16 font-semibold">URI</div>
141
141
-
<A
142
142
-
href={`/at://${label.uri.replace("at://", "")}`}
143
143
-
class="text-blue-400 hover:underline active:underline"
144
144
-
>
145
145
-
{label.uri}
146
146
-
</A>
147
147
-
</div>
148
148
-
<Show when={label.cid}>
149
149
-
<div class="flex items-center gap-x-2">
150
150
-
<div class="min-w-16 font-semibold">CID</div>
151
151
-
{label.cid}
152
152
-
</div>
153
153
-
</Show>
154
154
-
<div class="flex items-center gap-x-2">
155
155
-
<div class="min-w-16 font-semibold">Label</div>
156
156
-
{label.val}
157
157
-
</div>
158
158
-
<div class="flex items-center gap-x-2">
159
159
-
<div class="min-w-16 font-semibold">Created</div>
160
160
-
{localDateFromTimestamp(new Date(label.cts).getTime())}
161
161
-
</div>
162
162
-
<Show when={label.exp}>
163
163
-
{(exp) => (
164
164
-
<div class="flex items-center gap-x-2">
165
165
-
<div class="min-w-16 font-semibold">Expires</div>
166
166
-
{localDateFromTimestamp(new Date(exp()).getTime())}
167
167
-
</div>
168
168
-
)}
169
169
-
</Show>
170
170
-
</div>
171
171
-
<Show when={label.neg}>
172
172
-
<div class="iconify lucide--minus shrink-0 text-lg text-red-500 dark:text-red-400" />
173
173
-
</Show>
256
256
+
257
257
+
<Show when={!loading() || labels().length > 0}>
258
258
+
<Show when={filteredLabels().length > 0}>
259
259
+
<div class="grid gap-2">
260
260
+
<For each={filteredLabels()}>{(label) => <LabelCard label={label} />}</For>
261
261
+
</div>
262
262
+
</Show>
263
263
+
264
264
+
<Show when={labels().length > 0 && filteredLabels().length === 0}>
265
265
+
<div class="flex flex-col items-center justify-center py-8 text-center">
266
266
+
<span class="iconify lucide--search-x mb-2 text-3xl text-neutral-400" />
267
267
+
<p class="text-sm text-neutral-600 dark:text-neutral-400">
268
268
+
No labels match your filter
269
269
+
</p>
270
270
+
</div>
271
271
+
</Show>
272
272
+
273
273
+
<Show when={labels().length === 0 && !loading()}>
274
274
+
<div class="flex flex-col items-center justify-center py-8 text-center">
275
275
+
<span class="iconify lucide--inbox mb-2 text-3xl text-neutral-400" />
276
276
+
<p class="text-sm text-neutral-600 dark:text-neutral-400">No labels found</p>
174
277
</div>
175
175
-
)}
176
176
-
</For>
278
278
+
</Show>
279
279
+
</Show>
177
280
</div>
178
178
-
</Show>
179
179
-
<Show when={!labels().length && !loading() && searchParams.uriPatterns}>
180
180
-
<div class="mt-2">No results</div>
181
281
</Show>
182
282
</div>
183
283
);
+8
-1
src/views/record.tsx
···
9
9
import { Backlinks } from "../components/backlinks.jsx";
10
10
import { Button } from "../components/button.jsx";
11
11
import { RecordEditor, setPlaceholder } from "../components/create.jsx";
12
12
-
import { CopyMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx";
12
12
+
import {
13
13
+
CopyMenu,
14
14
+
DropdownMenu,
15
15
+
MenuProvider,
16
16
+
MenuSeparator,
17
17
+
NavMenu,
18
18
+
} from "../components/dropdown.jsx";
13
19
import { JSONValue } from "../components/json.jsx";
14
20
import { LexiconSchemaView } from "../components/lexicon-schema.jsx";
15
21
import { agent } from "../components/login.jsx";
···
238
244
<Show when={record()?.cid}>
239
245
{(cid) => <CopyMenu content={cid()} label="Copy CID" icon="lucide--copy" />}
240
246
</Show>
247
247
+
<MenuSeparator />
241
248
<Show when={externalLink()}>
242
249
{(externalLink) => (
243
250
<NavMenu
+17
-7
src/views/repo.tsx
···
19
19
CopyMenu,
20
20
DropdownMenu,
21
21
MenuProvider,
22
22
+
MenuSeparator,
22
23
NavMenu,
23
24
} from "../components/dropdown.jsx";
24
25
import { setPDS } from "../components/navbar.jsx";
···
31
32
import Tooltip from "../components/tooltip.jsx";
32
33
import {
33
34
didDocCache,
35
35
+
labelerCache,
34
36
resolveHandle,
35
37
resolveLexiconAuthority,
36
38
resolvePDS,
···
293
295
label="Jetstream"
294
296
icon="lucide--radio-tower"
295
297
/>
298
298
+
<Show when={params.repo in labelerCache}>
299
299
+
<NavMenu
300
300
+
href={`/labels?did=${params.repo}&uriPatterns=*`}
301
301
+
label="Labels"
302
302
+
icon="lucide--tag"
303
303
+
/>
304
304
+
</Show>
305
305
+
<Show when={error()?.length === 0 || error() === undefined}>
306
306
+
<ActionMenu
307
307
+
label="Export Repo"
308
308
+
icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"}
309
309
+
onClick={() => downloadRepo()}
310
310
+
/>
311
311
+
</Show>
312
312
+
<MenuSeparator />
296
313
<NavMenu
297
314
href={
298
315
did.startsWith("did:plc") ?
···
309
326
newTab
310
327
label="Audit Log"
311
328
icon="lucide--external-link"
312
312
-
/>
313
313
-
</Show>
314
314
-
<Show when={error()?.length === 0 || error() === undefined}>
315
315
-
<ActionMenu
316
316
-
label="Export Repo"
317
317
-
icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"}
318
318
-
onClick={() => downloadRepo()}
319
329
/>
320
330
</Show>
321
331
</DropdownMenu>