learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
1import { onCleanup, onMount, Show, splitProps } from "solid-js";
2import type { Component, JSX } from "solid-js";
3import { Motion, Presence } from "solid-motionone";
4
5type RightPanelProps = {
6 open: boolean;
7 onClose: () => void;
8 title?: string;
9 width?: string;
10 children: JSX.Element;
11 class?: string;
12};
13
14export const RightPanel: Component<RightPanelProps> = (props) => {
15 const [local, _others] = splitProps(props, ["open", "onClose", "title", "width", "children", "class"]);
16 const width = () => local.width ?? "400px";
17
18 const handleKeyDown = (e: KeyboardEvent) => {
19 if (e.key === "Escape" && local.open) {
20 local.onClose();
21 }
22 };
23
24 onMount(() => {
25 document.addEventListener("keydown", handleKeyDown);
26 });
27
28 onCleanup(() => {
29 document.removeEventListener("keydown", handleKeyDown);
30 });
31
32 return (
33 <Presence>
34 <Show when={local.open}>
35 {/* Backdrop */}
36 <Motion.div
37 initial={{ opacity: 0 }}
38 animate={{ opacity: 1 }}
39 exit={{ opacity: 0 }}
40 transition={{ duration: 0.15 }}
41 class="fixed inset-0 z-40 bg-black/40"
42 onClick={local.onClose}
43 aria-hidden="true" />
44 {/* Panel */}
45 <Motion.div
46 initial={{ x: "100%" }}
47 animate={{ x: 0 }}
48 exit={{ x: "100%" }}
49 transition={{ duration: 0.25, easing: [0.22, 1, 0.36, 1] }}
50 class={`fixed top-0 right-0 bottom-0 z-50 bg-gray-900 border-l border-gray-800 shadow-2xl flex flex-col ${
51 local.class || ""
52 }`}
53 style={{ width: width() }}
54 role="dialog"
55 aria-modal="true"
56 aria-label={local.title || "Panel"}>
57 {/* Header */}
58 <div class="h-16 flex items-center justify-between px-6 border-b border-gray-800">
59 <Show when={local.title}>
60 <h2 class="text-lg font-semibold text-white">{local.title}</h2>
61 </Show>
62 <button
63 onClick={() => local.onClose()}
64 class="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded transition-colors ml-auto"
65 aria-label="Close panel">
66 <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
67 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
68 </svg>
69 </button>
70 </div>
71 {/* Content */}
72 <div class="flex-1 overflow-y-auto p-6 text-gray-300">{local.children}</div>
73 </Motion.div>
74 </Show>
75 </Presence>
76 );
77};