BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

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