tangled
alpha
login
or
join now
vielle.dev
/
pdsls
forked from
pds.ls/pdsls
0
fork
atom
atproto explorer
0
fork
atom
overview
issues
pulls
pipelines
menu component
handle.invalid
6 months ago
ba872c83
72a586ce
+135
-94
3 changed files
expand all
collapse all
unified
split
src
components
dropdown.tsx
navbar.tsx
layout.tsx
+101
src/components/dropdown.tsx
···
1
1
+
import { A } from "@solidjs/router";
2
2
+
import {
3
3
+
Accessor,
4
4
+
createContext,
5
5
+
createSignal,
6
6
+
JSX,
7
7
+
onCleanup,
8
8
+
onMount,
9
9
+
Setter,
10
10
+
Show,
11
11
+
useContext,
12
12
+
} from "solid-js";
13
13
+
import { addToClipboard } from "../utils/copy";
14
14
+
15
15
+
const MenuContext = createContext<{
16
16
+
showMenu: Accessor<boolean>;
17
17
+
setShowMenu: Setter<boolean>;
18
18
+
}>();
19
19
+
20
20
+
export const MenuProvider = (props: { children?: JSX.Element }) => {
21
21
+
const [showMenu, setShowMenu] = createSignal(false);
22
22
+
const value = { showMenu, setShowMenu };
23
23
+
24
24
+
return <MenuContext.Provider value={value}>{props.children}</MenuContext.Provider>;
25
25
+
};
26
26
+
27
27
+
export const CopyMenu = (props: { copyContent: string; label: string }) => {
28
28
+
const ctx = useContext(MenuContext);
29
29
+
30
30
+
return (
31
31
+
<button
32
32
+
onClick={() => {
33
33
+
addToClipboard(props.copyContent);
34
34
+
ctx?.setShowMenu(false);
35
35
+
}}
36
36
+
class="flex rounded-lg p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700"
37
37
+
>
38
38
+
{props.label}
39
39
+
</button>
40
40
+
);
41
41
+
};
42
42
+
43
43
+
export const NavMenu = (props: { href: string; label: string; icon: string }) => {
44
44
+
const ctx = useContext(MenuContext);
45
45
+
46
46
+
return (
47
47
+
<A
48
48
+
href={props.href}
49
49
+
onClick={() => ctx?.setShowMenu(false)}
50
50
+
class="flex items-center gap-1 rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700"
51
51
+
>
52
52
+
<span class={"iconify " + props.icon}></span>
53
53
+
<span>{props.label}</span>
54
54
+
</A>
55
55
+
);
56
56
+
};
57
57
+
58
58
+
export const DropdownMenu = (props: {
59
59
+
icon: string;
60
60
+
buttonClass?: string;
61
61
+
menuClass?: string;
62
62
+
children?: JSX.Element;
63
63
+
}) => {
64
64
+
const ctx = useContext(MenuContext);
65
65
+
const [menu, setMenu] = createSignal<HTMLDivElement>();
66
66
+
const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>();
67
67
+
68
68
+
const clickEvent = (event: MouseEvent) => {
69
69
+
const target = event.target as Node;
70
70
+
if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false);
71
71
+
};
72
72
+
73
73
+
onMount(() => window.addEventListener("click", clickEvent));
74
74
+
onCleanup(() => window.removeEventListener("click", clickEvent));
75
75
+
76
76
+
return (
77
77
+
<div class="relative">
78
78
+
<button
79
79
+
class={
80
80
+
"flex items-center hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700 " +
81
81
+
props.buttonClass
82
82
+
}
83
83
+
ref={setMenuButton}
84
84
+
onClick={() => ctx?.setShowMenu(!ctx?.showMenu())}
85
85
+
>
86
86
+
<span class={"iconify " + props.icon}></span>
87
87
+
</button>
88
88
+
<Show when={ctx?.showMenu()}>
89
89
+
<div
90
90
+
ref={setMenu}
91
91
+
class={
92
92
+
"dark:bg-dark-300 absolute right-0 z-20 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 shadow-md dark:border-neutral-700 " +
93
93
+
props.menuClass
94
94
+
}
95
95
+
>
96
96
+
{props.children}
97
97
+
</div>
98
98
+
</Show>
99
99
+
</div>
100
100
+
);
101
101
+
};
+22
-55
src/components/navbar.tsx
···
1
1
import { Did, Handle } from "@atcute/lexicons";
2
2
import { A, Params, useLocation } from "@solidjs/router";
3
3
-
import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js";
3
3
+
import { createEffect, createSignal, Show } from "solid-js";
4
4
import { didDocCache, labelerCache, validateHandle } from "../utils/api";
5
5
-
import { addToClipboard } from "../utils/copy";
5
5
+
import { CopyMenu, DropdownMenu, MenuProvider } from "./dropdown";
6
6
import Tooltip from "./tooltip";
7
7
8
8
export const [pds, setPDS] = createSignal<string>();
···
32
32
const [validHandle, setValidHandle] = createSignal<boolean | undefined>(undefined);
33
33
const [fullCid, setFullCid] = createSignal(false);
34
34
const [showHandle, setShowHandle] = createSignal(localStorage.showHandle === "true");
35
35
-
const [showCopyMenu, setShowCopyMenu] = createSignal(false);
36
36
-
const [copyMenu, setCopyMenu] = createSignal<HTMLDivElement>();
37
37
-
const [copyButton, setCopyButton] = createSignal<HTMLButtonElement>();
38
35
39
36
createEffect(() => {
40
37
if (cid() !== undefined) setFullCid(false);
···
54
51
}
55
52
});
56
53
57
57
-
const clickEvent = (event: MouseEvent) => {
58
58
-
const target = event.target as Node;
59
59
-
if (!copyButton()?.contains(target) && !copyMenu()?.contains(target)) setShowCopyMenu(false);
60
60
-
};
61
61
-
62
62
-
onMount(() => window.addEventListener("click", clickEvent));
63
63
-
onCleanup(() => window.removeEventListener("click", clickEvent));
64
64
-
65
65
-
const CopyButton = (props: { copyContent: string; label: string }) => {
66
66
-
return (
67
67
-
<button
68
68
-
onClick={() => {
69
69
-
addToClipboard(props.copyContent);
70
70
-
setShowCopyMenu(false);
71
71
-
}}
72
72
-
class="flex rounded-lg p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700"
73
73
-
>
74
74
-
{props.label}
75
75
-
</button>
76
76
-
);
77
77
-
};
78
78
-
79
54
return (
80
55
<nav class="mt-4 flex w-[22rem] flex-col text-sm wrap-anywhere sm:w-[24rem]">
81
56
<div class="relative flex items-center justify-between gap-1">
···
98
73
</Show>
99
74
</Show>
100
75
</div>
101
101
-
<div class="relative">
102
102
-
<button
103
103
-
class="flex items-center rounded p-0.5 hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700"
104
104
-
ref={setCopyButton}
105
105
-
onClick={() => setShowCopyMenu(!showCopyMenu())}
76
76
+
<MenuProvider>
77
77
+
<DropdownMenu
78
78
+
icon="lucide--copy text-base"
79
79
+
buttonClass="rounded p-0.5"
80
80
+
menuClass="top-6 p-2 text-xs"
106
81
>
107
107
-
<span class="iconify lucide--copy text-base"></span>
108
108
-
</button>
109
109
-
<Show when={showCopyMenu()}>
110
110
-
<div
111
111
-
ref={setCopyMenu}
112
112
-
class="dark:bg-dark-300 absolute top-6 right-0 z-20 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs shadow-md dark:border-neutral-700"
113
113
-
>
114
114
-
<Show when={pds()}>
115
115
-
<CopyButton copyContent={pds()!} label="Copy PDS" />
116
116
-
</Show>
117
117
-
<Show when={props.params.repo}>
118
118
-
<CopyButton copyContent={props.params.repo} label="Copy DID" />
119
119
-
<CopyButton
120
120
-
copyContent={`at://${props.params.repo}${props.params.collection ? `/${props.params.collection}` : ""}${props.params.rkey ? `/${props.params.rkey}` : ""}`}
121
121
-
label="Copy AT URI"
122
122
-
/>
123
123
-
</Show>
124
124
-
<Show when={props.params.rkey && cid()}>
125
125
-
<CopyButton copyContent={cid()!} label="Copy CID" />
126
126
-
</Show>
127
127
-
</div>
128
128
-
</Show>
129
129
-
</div>
82
82
+
<Show when={pds()}>
83
83
+
<CopyMenu copyContent={pds()!} label="Copy PDS" />
84
84
+
</Show>
85
85
+
<Show when={props.params.repo}>
86
86
+
<CopyMenu copyContent={props.params.repo} label="Copy DID" />
87
87
+
<CopyMenu
88
88
+
copyContent={`at://${props.params.repo}${props.params.collection ? `/${props.params.collection}` : ""}${props.params.rkey ? `/${props.params.rkey}` : ""}`}
89
89
+
label="Copy AT URI"
90
90
+
/>
91
91
+
</Show>
92
92
+
<Show when={props.params.rkey && cid()}>
93
93
+
<CopyMenu copyContent={cid()!} label="Copy CID" />
94
94
+
</Show>
95
95
+
</DropdownMenu>
96
96
+
</MenuProvider>
130
97
</div>
131
98
<div class="flex flex-col flex-wrap">
132
99
<Show when={props.params.repo}>
+12
-39
src/layout.tsx
···
4
4
import { createEffect, createSignal, 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
8
import { agent } from "./components/login.jsx";
8
9
import { NavBar } from "./components/navbar.jsx";
9
10
import { Search } from "./components/search.jsx";
···
20
21
const location = useLocation();
21
22
const navigate = useNavigate();
22
23
let timeout: number;
23
23
-
const [showMenu, setShowMenu] = createSignal(false);
24
24
-
const [menu, setMenu] = createSignal<HTMLDivElement>();
25
25
-
const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>();
26
24
27
25
createEffect(async () => {
28
26
if (props.params.repo && !props.params.repo.startsWith("did:")) {
···
40
38
41
39
onMount(() => {
42
40
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent);
43
43
-
window.addEventListener("click", (ev) => {
44
44
-
if (!menuButton()?.contains(ev.target as Node) && !menu()?.contains(ev.target as Node))
45
45
-
setShowMenu(false);
46
46
-
});
47
41
});
48
42
49
49
-
const NavButton = (props: { href: string; label: string; icon: string }) => {
50
50
-
return (
51
51
-
<A
52
52
-
href={props.href}
53
53
-
onClick={() => setShowMenu(false)}
54
54
-
class="flex items-center gap-1 rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700"
55
55
-
>
56
56
-
<span class={"iconify " + props.icon}></span>
57
57
-
<span>{props.label}</span>
58
58
-
</A>
59
59
-
);
60
60
-
};
61
61
-
62
43
return (
63
44
<div id="main" class="m-4 flex flex-col items-center text-neutral-900 dark:text-neutral-200">
64
45
<MetaProvider>
···
80
61
<RecordEditor create={true} />
81
62
</Show>
82
63
<AccountManager />
83
83
-
<div class="relative">
84
84
-
<button
85
85
-
onClick={() => setShowMenu(!showMenu())}
86
86
-
ref={setMenuButton}
87
87
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700"
64
64
+
<MenuProvider>
65
65
+
<DropdownMenu
66
66
+
icon="lucide--menu text-xl"
67
67
+
buttonClass="rounded-lg p-1"
68
68
+
menuClass="top-8 p-3 text-sm"
88
69
>
89
89
-
<span class="iconify lucide--menu text-xl"></span>
90
90
-
</button>
91
91
-
<Show when={showMenu()}>
92
92
-
<div
93
93
-
ref={setMenu}
94
94
-
class="dark:bg-dark-300 absolute top-8 right-0 z-20 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 text-sm shadow-md dark:border-neutral-700"
95
95
-
>
96
96
-
<NavButton href="/jetstream" label="Jetstream" icon="lucide--radio-tower" />
97
97
-
<NavButton href="/firehose" label="Firehose" icon="lucide--waves" />
98
98
-
<NavButton href="/settings" label="Settings" icon="lucide--settings" />
99
99
-
<ThemeSelection />
100
100
-
</div>
101
101
-
</Show>
102
102
-
</div>
70
70
+
<NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" />
71
71
+
<NavMenu href="/firehose" label="Firehose" icon="lucide--waves" />
72
72
+
<NavMenu href="/settings" label="Settings" icon="lucide--settings" />
73
73
+
<ThemeSelection />
74
74
+
</DropdownMenu>
75
75
+
</MenuProvider>
103
76
</div>
104
77
</header>
105
78
<div class="mb-4 flex max-w-full min-w-[22rem] flex-col items-center text-pretty sm:min-w-[24rem] md:max-w-[48rem]">