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.

feat: add tab navigation to column panels

+152 -58
+38
src/components/deck/AddColumnPanel.test.tsx
··· 1 + import { fireEvent, render, screen, waitFor } from "@solidjs/testing-library"; 2 + import { beforeEach, describe, expect, it, vi } from "vitest"; 3 + import { AddColumnPanel } from "./AddColumnPanel"; 4 + 5 + const getPreferencesMock = vi.hoisted(() => vi.fn()); 6 + 7 + vi.mock("$/lib/api/feeds", () => ({ getPreferences: getPreferencesMock })); 8 + vi.mock("@tauri-apps/plugin-log", () => ({ error: vi.fn(), info: vi.fn(), warn: vi.fn() })); 9 + 10 + describe("AddColumnPanel", () => { 11 + beforeEach(() => { 12 + vi.resetAllMocks(); 13 + getPreferencesMock.mockResolvedValue({ 14 + feedViewPrefs: [], 15 + savedFeeds: [{ id: "following", pinned: true, type: "timeline", value: "following" }], 16 + }); 17 + }); 18 + 19 + it("renders the picker when open", async () => { 20 + render(() => <AddColumnPanel open={true} onAdd={vi.fn()} onClose={vi.fn()} />); 21 + 22 + expect(await screen.findByRole("dialog")).toBeInTheDocument(); 23 + expect(await screen.findByText("Add column")).toBeInTheDocument(); 24 + expect(await screen.findByText("Following")).toBeInTheDocument(); 25 + }); 26 + 27 + it("submits the selected feed as a deck column", async () => { 28 + const onAdd = vi.fn(); 29 + 30 + render(() => <AddColumnPanel open={true} onAdd={onAdd} onClose={vi.fn()} />); 31 + 32 + fireEvent.click(await screen.findByRole("button", { name: /timeline/i })); 33 + 34 + await waitFor(() => 35 + expect(onAdd).toHaveBeenCalledWith("feed", JSON.stringify({ feedType: "timeline", feedUri: "following" })) 36 + ); 37 + }); 38 + });
+106 -49
src/components/deck/AddColumnPanel.tsx
··· 3 3 import { getFeedName } from "$/lib/feeds"; 4 4 import type { SavedFeedItem } from "$/lib/types"; 5 5 import * as logger from "@tauri-apps/plugin-log"; 6 - import { createSignal, For, Match, onMount, Show, Switch } from "solid-js"; 6 + import { createEffect, createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js"; 7 + import { Portal } from "solid-js/web"; 7 8 import { Motion, Presence } from "solid-motionone"; 8 9 import { Icon } from "../shared/Icon"; 9 10 ··· 177 178 178 179 function AddColumnPanelHeader(props: { onClose: () => void }) { 179 180 return ( 180 - <div class="flex shrink-0 items-center justify-between gap-3 border-b border-white/5 px-5 py-4"> 181 - <p class="m-0 text-sm font-semibold text-on-surface">Add column</p> 181 + <div class="flex shrink-0 items-center justify-between gap-3 px-5 py-4 shadow-[inset_0_-1px_0_rgba(255,255,255,0.04)]"> 182 + <div> 183 + <p id="add-column-panel-title" class="m-0 text-sm font-semibold text-on-surface">Add column</p> 184 + <p class="m-0 mt-1 text-xs uppercase tracking-[0.12em] text-on-surface-variant"> 185 + Feed, explorer, or diagnostics 186 + </p> 187 + </div> 182 188 <button 183 189 type="button" 184 190 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-white/6 hover:text-on-surface" ··· 190 196 ); 191 197 } 192 198 199 + function AddColumnPanelTabs( 200 + props: { 201 + activeTab: PanelTab; 202 + onTabChange: (tab: PanelTab) => void; 203 + tabs: Array<{ icon: string; id: PanelTab; label: string }>; 204 + }, 205 + ) { 206 + return ( 207 + <div class="flex shrink-0 gap-1 px-5 py-3"> 208 + <For each={props.tabs}> 209 + {(tab) => ( 210 + <button 211 + type="button" 212 + class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border-0 px-3 py-2 text-xs font-medium transition duration-150" 213 + classList={{ 214 + "bg-primary/15 text-primary": props.activeTab === tab.id, 215 + "bg-transparent text-on-surface-variant hover:bg-white/5 hover:text-on-surface": 216 + props.activeTab !== tab.id, 217 + }} 218 + onClick={() => props.onTabChange(tab.id)}> 219 + <span class="flex items-center"> 220 + <i class={tab.icon} /> 221 + </span> 222 + {tab.label} 223 + </button> 224 + )} 225 + </For> 226 + </div> 227 + ); 228 + } 229 + 230 + function AddColumnPanelBody( 231 + props: { 232 + activeTab: PanelTab; 233 + onClose: () => void; 234 + onDiagnosticsSubmit: (did: string) => void; 235 + onExplorerSubmit: (uri: string) => void; 236 + onFeedSelect: (feed: SavedFeedItem) => void; 237 + onTabChange: (tab: PanelTab) => void; 238 + tabs: Array<{ icon: string; id: PanelTab; label: string }>; 239 + }, 240 + ) { 241 + return ( 242 + <Motion.aside 243 + role="dialog" 244 + aria-modal="true" 245 + aria-labelledby="add-column-panel-title" 246 + class="relative z-10 flex h-full w-full max-w-88 flex-col bg-(--surface-container-highest) shadow-[-18px_0_48px_rgba(0,0,0,0.38)] backdrop-blur-[20px]" 247 + initial={{ opacity: 0, x: 32 }} 248 + animate={{ opacity: 1, x: 0 }} 249 + exit={{ opacity: 0, x: 40 }} 250 + transition={{ duration: 0.22, easing: [0.32, 0.72, 0, 1] }}> 251 + <AddColumnPanelHeader onClose={props.onClose} /> 252 + <AddColumnPanelTabs activeTab={props.activeTab} tabs={props.tabs} onTabChange={props.onTabChange} /> 253 + <PanelContent 254 + tab={props.activeTab} 255 + onFeedSelect={props.onFeedSelect} 256 + onExplorerSubmit={props.onExplorerSubmit} 257 + onDiagnosticsSubmit={props.onDiagnosticsSubmit} /> 258 + </Motion.aside> 259 + ); 260 + } 261 + 193 262 export function AddColumnPanel(props: AddColumnPanelProps) { 194 263 const [activeTab, setActiveTab] = createSignal<PanelTab>("feed"); 195 264 ··· 214 283 { icon: "i-ri-stethoscope-line", id: "diagnostics", label: "Diagnostics" }, 215 284 ]; 216 285 286 + createEffect(() => { 287 + if (!props.open) { 288 + setActiveTab("feed"); 289 + return; 290 + } 291 + 292 + const handleKeyDown = (event: KeyboardEvent) => { 293 + if (event.key === "Escape") { 294 + props.onClose(); 295 + } 296 + }; 297 + 298 + globalThis.addEventListener("keydown", handleKeyDown); 299 + onCleanup(() => globalThis.removeEventListener("keydown", handleKeyDown)); 300 + }); 301 + 217 302 return ( 218 303 <Presence exitBeforeEnter> 219 304 <Show when={props.open}> 220 - {/* Backdrop */} 221 - <Motion.div 222 - class="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm" 223 - initial={{ opacity: 0 }} 224 - animate={{ opacity: 1 }} 225 - exit={{ opacity: 0 }} 226 - transition={{ duration: 0.15 }} 227 - onClick={() => props.onClose()} /> 228 - 229 - {/* Panel */} 230 - <Motion.aside 231 - class="fixed right-0 top-0 z-50 flex h-full w-80 flex-col bg-surface-container shadow-[-8px_0_32px_rgba(0,0,0,0.4)]" 232 - initial={{ x: "100%" }} 233 - animate={{ x: "0%" }} 234 - exit={{ x: "100%" }} 235 - transition={{ duration: 0.22, easing: [0.32, 0.72, 0, 1] }}> 236 - {/* Header */} 237 - <AddColumnPanelHeader onClose={props.onClose} /> 305 + <Portal> 306 + <div class="fixed inset-0 z-50 flex justify-end"> 307 + <Motion.div 308 + class="absolute inset-0 bg-black/45 backdrop-blur-[20px]" 309 + initial={{ opacity: 0 }} 310 + animate={{ opacity: 1 }} 311 + exit={{ opacity: 0 }} 312 + transition={{ duration: 0.15 }} 313 + onClick={() => props.onClose()} /> 238 314 239 - {/* Tabs */} 240 - <div class="flex shrink-0 gap-1 px-4 py-3"> 241 - <For each={tabs}> 242 - {(tab) => ( 243 - <button 244 - type="button" 245 - class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border-0 px-3 py-2 text-xs font-medium transition duration-150" 246 - classList={{ 247 - "bg-primary/15 text-primary": activeTab() === tab.id, 248 - "bg-transparent text-on-surface-variant hover:bg-white/5 hover:text-on-surface": 249 - activeTab() !== tab.id, 250 - }} 251 - onClick={() => setActiveTab(tab.id)}> 252 - <span class="flex items-center"> 253 - <i class={tab.icon} /> 254 - </span> 255 - {tab.label} 256 - </button> 257 - )} 258 - </For> 315 + <AddColumnPanelBody 316 + activeTab={activeTab()} 317 + tabs={tabs} 318 + onClose={props.onClose} 319 + onTabChange={setActiveTab} 320 + onFeedSelect={handleFeedSelect} 321 + onExplorerSubmit={handleExplorerSubmit} 322 + onDiagnosticsSubmit={handleDiagnosticsSubmit} /> 259 323 </div> 260 - 261 - {/* Content */} 262 - <PanelContent 263 - tab={activeTab()} 264 - onFeedSelect={handleFeedSelect} 265 - onExplorerSubmit={handleExplorerSubmit} 266 - onDiagnosticsSubmit={handleDiagnosticsSubmit} /> 267 - </Motion.aside> 324 + </Portal> 268 325 </Show> 269 326 </Presence> 270 327 );
+1 -1
src/components/deck/DeckColumn.tsx
··· 200 200 201 201 return ( 202 202 <section 203 - class="flex shrink-0 flex-col overflow-hidden rounded-2xl bg-[rgba(8,8,8,0.32)] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.03)]" 203 + class="flex h-full shrink-0 flex-col overflow-hidden rounded-2xl bg-[rgba(8,8,8,0.32)] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.03)]" 204 204 style={{ width: `${widthPx()}px` }}> 205 205 <ColumnHeader 206 206 column={props.column}
+7 -8
src/components/deck/DeckWorkspace.tsx
··· 14 14 15 15 function DeckToolbar(props: { columnCount: number; onAdd: () => void }) { 16 16 return ( 17 - <div class="flex shrink-0 items-center justify-between gap-4 pb-4"> 17 + <div class="flex shrink-0 items-center justify-between gap-4 pb-5"> 18 18 <div class="min-w-0"> 19 19 <p class="m-0 text-xl font-semibold tracking-tight text-on-surface">Deck</p> 20 20 <p class="m-0 mt-0.5 text-xs uppercase tracking-[0.12em] text-on-surface-variant"> ··· 38 38 39 39 function EmptyDeck(props: { onAdd: () => void }) { 40 40 return ( 41 - <div class="flex h-64 flex-col items-center justify-center gap-4 text-center"> 41 + <div class="flex h-full min-h-104 flex-col items-center justify-center gap-4 rounded-[1.75rem] bg-white/3 px-6 text-center shadow-[inset_0_0_0_1px_rgba(255,255,255,0.035)]"> 42 42 <span class="flex items-center text-[2.5rem] text-on-surface-variant opacity-30"> 43 43 <i class="i-ri-layout-column-line" /> 44 44 </span> ··· 72 72 }, 73 73 ) { 74 74 return ( 75 - <div class="flex h-full min-h-96 gap-3 pb-2"> 75 + <div class="flex h-full min-h-0 items-stretch gap-4 pb-3"> 76 76 <For each={props.columns}> 77 77 {(column) => ( 78 78 <Motion.div 79 - class="flex shrink-0" 80 - style={{ height: "calc(100vh - 12rem)" }} 79 + class="flex h-full shrink-0" 81 80 initial={{ opacity: 0, scale: 0.95 }} 82 81 animate={{ opacity: 1, scale: 1 }} 83 82 transition={{ duration: 0.18, easing: [0.34, 1.56, 0.64, 1] }}> ··· 228 227 }); 229 228 230 229 return ( 231 - <div class="relative flex min-h-0 min-w-0 flex-col"> 230 + <div class="relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden px-6 py-5 max-[900px]:px-4 max-[900px]:py-4 max-[640px]:px-3 max-[640px]:py-3"> 232 231 <DeckToolbar columnCount={state.columns.length} onAdd={() => setState("addPanelOpen", true)} /> 233 232 234 - <div class="min-h-0 flex-1 overflow-x-auto overscroll-contain"> 233 + <div class="min-h-0 flex-1 overflow-x-auto overflow-y-hidden overscroll-contain"> 235 234 <Show when={state.loading}> 236 - <div class="flex h-64 items-center justify-center"> 235 + <div class="flex h-full min-h-80 items-center justify-center"> 237 236 <Icon iconClass="i-ri-loader-4-line animate-spin text-2xl text-on-surface-variant" /> 238 237 </div> 239 238 </Show>