BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
at main 227 lines 8.2 kB view raw
1import type { ColumnKind } from "$/lib/api/types/columns"; 2import type { SearchMode } from "$/lib/api/types/search"; 3import { createEffect, createSignal, For, Match, onCleanup, Show, splitProps, Switch } from "solid-js"; 4import { Portal } from "solid-js/web"; 5import { Motion, Presence } from "solid-motionone"; 6import { Icon } from "../shared/Icon"; 7import { DiagnosticsPicker, ExplorerPicker, FeedPicker, MessagesPicker } from "./ColumnPicker"; 8import { ProfilePicker } from "./ColumnPicker/ProfileColumnPicker"; 9import { SearchPicker } from "./ColumnPicker/SearchPicker"; 10import type { FeedPickerSelection, ProfileSelection } from "./types"; 11 12type AddColumnPanelProps = { onAdd: (kind: ColumnKind, config: string) => void; onClose: () => void; open: boolean }; 13 14type PanelTab = ColumnKind; 15 16type PanelSubmissionHandlers = { 17 onDiagnosticsSubmit: (did: string) => void; 18 onExplorerSubmit: (uri: string) => void; 19 onFeedSelect: (selection: FeedPickerSelection) => void; 20 onMessagesSubmit: () => void; 21 onProfileSubmit: (selection: ProfileSelection) => void; 22 onSearchSubmit: (query: string, mode: SearchMode) => void; 23}; 24 25function PanelContent(props: { handlers: PanelSubmissionHandlers; tab: PanelTab }) { 26 return ( 27 <div class="min-h-0 flex-1 overflow-y-auto px-4 pb-6"> 28 <Switch> 29 <Match when={props.tab === "feed"}> 30 <FeedPicker onSelect={props.handlers.onFeedSelect} /> 31 </Match> 32 33 <Match when={props.tab === "explorer"}> 34 <ExplorerPicker onSubmit={props.handlers.onExplorerSubmit} /> 35 </Match> 36 37 <Match when={props.tab === "diagnostics"}> 38 <DiagnosticsPicker onSubmit={props.handlers.onDiagnosticsSubmit} /> 39 </Match> 40 41 <Match when={props.tab === "messages"}> 42 <MessagesPicker onSubmit={props.handlers.onMessagesSubmit} /> 43 </Match> 44 45 <Match when={props.tab === "search"}> 46 <SearchPicker onSubmit={props.handlers.onSearchSubmit} /> 47 </Match> 48 49 <Match when={props.tab === "profile"}> 50 <ProfilePicker onSubmit={props.handlers.onProfileSubmit} /> 51 </Match> 52 </Switch> 53 </div> 54 ); 55} 56 57function AddColumnPanelHeader(props: { onClose: () => void }) { 58 return ( 59 <div class="flex shrink-0 items-center justify-between gap-3 px-5 py-4 shadow-(--inset-shadow)"> 60 <div> 61 <p id="add-column-panel-title" class="m-0 text-sm font-semibold text-on-surface">Add column</p> 62 <p class="m-0 mt-1 text-xs uppercase tracking-[0.12em] text-on-surface-variant">Choose a view</p> 63 </div> 64 <button 65 type="button" 66 class="flex h-8 w-8 items-center justify-center rounded-full border-0 bg-transparent text-on-surface-variant transition duration-150 hover:bg-surface-bright hover:text-on-surface" 67 aria-label="Close panel" 68 onClick={() => props.onClose()}> 69 <Icon kind="close" /> 70 </button> 71 </div> 72 ); 73} 74 75type AddColumnPanelTabsProps = { 76 activeTab: PanelTab; 77 onTabChange: (tab: PanelTab) => void; 78 tabs: Array<{ icon: string; id: PanelTab; label: string }>; 79}; 80 81function AddColumnPanelTabs(props: AddColumnPanelTabsProps) { 82 return ( 83 <div class="grid shrink-0 grid-cols-2 gap-1 px-5 py-3"> 84 <For each={props.tabs}> 85 {(tab) => ( 86 <button 87 type="button" 88 class="flex items-center justify-center gap-1.5 rounded-lg border-0 px-3 py-2 text-xs font-medium transition duration-150" 89 classList={{ 90 "bg-primary/15 text-primary": props.activeTab === tab.id, 91 "bg-transparent text-on-surface-variant hover:bg-surface-bright hover:text-on-surface": 92 props.activeTab !== tab.id, 93 }} 94 onClick={() => props.onTabChange(tab.id)}> 95 <span class="flex items-center"> 96 <i class={tab.icon} /> 97 </span> 98 {tab.label} 99 </button> 100 )} 101 </For> 102 </div> 103 ); 104} 105 106type AddColumnPanelFrame = { 107 activeTab: PanelTab; 108 onClose: () => void; 109 onTabChange: (tab: PanelTab) => void; 110 tabs: Array<{ icon: string; id: PanelTab; label: string }>; 111}; 112 113type AddColumnPanelBodyProps = { frame: AddColumnPanelFrame; handlers: PanelSubmissionHandlers }; 114 115function AddColumnPanelBody(props: AddColumnPanelBodyProps) { 116 const [frameProps, contentProps] = splitProps(props, ["frame"], ["handlers"]); 117 118 return ( 119 <Motion.aside 120 role="dialog" 121 aria-modal="true" 122 aria-labelledby="add-column-panel-title" 123 class="ui-overlay-card relative z-10 flex h-full w-full max-w-88 flex-col bg-surface-container-highest backdrop-blur-[20px]" 124 initial={{ opacity: 0, x: 32 }} 125 animate={{ opacity: 1, x: 0 }} 126 exit={{ opacity: 0, x: 40 }} 127 transition={{ duration: 0.22, easing: [0.32, 0.72, 0, 1] }}> 128 <AddColumnPanelHeader onClose={frameProps.frame.onClose} /> 129 <AddColumnPanelTabs 130 activeTab={frameProps.frame.activeTab} 131 tabs={frameProps.frame.tabs} 132 onTabChange={frameProps.frame.onTabChange} /> 133 <PanelContent tab={frameProps.frame.activeTab} handlers={contentProps.handlers} /> 134 </Motion.aside> 135 ); 136} 137 138export function AddColumnPanel(props: AddColumnPanelProps) { 139 const [panelState, panelActions] = splitProps(props, ["open"], ["onAdd", "onClose"]); 140 const [activeTab, setActiveTab] = createSignal<PanelTab>("feed"); 141 142 function handleFeedSelect(selection: FeedPickerSelection) { 143 const config = JSON.stringify({ 144 feedType: selection.feed.type, 145 feedUri: selection.feed.value, 146 title: selection.title, 147 }); 148 panelActions.onAdd("feed", config); 149 } 150 151 function handleExplorerSubmit(uri: string) { 152 const config = JSON.stringify({ targetUri: uri }); 153 panelActions.onAdd("explorer", config); 154 } 155 156 function handleDiagnosticsSubmit(did: string) { 157 const config = JSON.stringify({ did }); 158 panelActions.onAdd("diagnostics", config); 159 } 160 161 function handleMessagesSubmit() { 162 panelActions.onAdd("messages", JSON.stringify({})); 163 } 164 165 function handleSearchSubmit(query: string, mode: SearchMode) { 166 panelActions.onAdd("search", JSON.stringify({ mode, query })); 167 } 168 169 function handleProfileSubmit(selection: ProfileSelection) { 170 panelActions.onAdd("profile", JSON.stringify(selection)); 171 } 172 173 // TODO: use IconKind for Icon 174 const tabs: Array<{ icon: string; id: PanelTab; label: string }> = [ 175 { icon: "i-ri-rss-line", id: "feed", label: "Feed" }, 176 { icon: "i-ri-compass-discover-line", id: "explorer", label: "Explorer" }, 177 { icon: "i-ri-stethoscope-line", id: "diagnostics", label: "Diagnostics" }, 178 { icon: "i-ri-message-3-line", id: "messages", label: "DMs" }, 179 { icon: "i-ri-search-line", id: "search", label: "Search" }, 180 { icon: "i-ri-user-3-line", id: "profile", label: "Profile" }, 181 ]; 182 183 function handleKeyDown(event: KeyboardEvent) { 184 if (event.key === "Escape") { 185 panelActions.onClose(); 186 } 187 } 188 189 createEffect(() => { 190 if (!panelState.open) { 191 setActiveTab("feed"); 192 return; 193 } 194 195 globalThis.addEventListener("keydown", handleKeyDown); 196 onCleanup(() => globalThis.removeEventListener("keydown", handleKeyDown)); 197 }); 198 199 return ( 200 <Presence exitBeforeEnter> 201 <Show when={panelState.open}> 202 <Portal> 203 <div class="fixed inset-0 z-50 flex justify-end"> 204 <Motion.div 205 class="ui-scrim absolute inset-0 backdrop-blur-[20px]" 206 initial={{ opacity: 0 }} 207 animate={{ opacity: 1 }} 208 exit={{ opacity: 0 }} 209 transition={{ duration: 0.15 }} 210 onClick={() => panelActions.onClose()} /> 211 212 <AddColumnPanelBody 213 frame={{ activeTab: activeTab(), tabs, onClose: panelActions.onClose, onTabChange: setActiveTab }} 214 handlers={{ 215 onDiagnosticsSubmit: handleDiagnosticsSubmit, 216 onExplorerSubmit: handleExplorerSubmit, 217 onFeedSelect: handleFeedSelect, 218 onMessagesSubmit: handleMessagesSubmit, 219 onProfileSubmit: handleProfileSubmit, 220 onSearchSubmit: handleSearchSubmit, 221 }} /> 222 </div> 223 </Portal> 224 </Show> 225 </Presence> 226 ); 227}