| 1 | export type WorkspaceTab = "threads" | "pullRequests"; |
| 2 | export type WorkspaceSearchMode = "resting" | "active" | "expanded"; |
| 3 | |
| 4 | export interface WorkspaceTabState { |
| 5 | readonly query: string; |
| 6 | readonly searchMode: WorkspaceSearchMode; |
| 7 | readonly scrollOffset: number; |
| 8 | readonly searchEntryOffset: number; |
| 9 | } |
| 10 | |
| 11 | export interface WorkspaceTabsState { |
| 12 | readonly selectedTab: WorkspaceTab; |
| 13 | readonly selectionRevision: number; |
| 14 | readonly tabs: Readonly<Record<WorkspaceTab, WorkspaceTabState>>; |
| 15 | } |
| 16 | |
| 17 | function emptyTab(): WorkspaceTabState { |
| 18 | return { query: "", searchMode: "resting", scrollOffset: 0, searchEntryOffset: 0 }; |
| 19 | } |
| 20 | |
| 21 | export function initialWorkspaceTabsState(): WorkspaceTabsState { |
| 22 | return { |
| 23 | selectedTab: "threads", |
| 24 | selectionRevision: 0, |
| 25 | tabs: { threads: emptyTab(), pullRequests: emptyTab() }, |
| 26 | }; |
| 27 | } |
| 28 | |
| 29 | export type WorkspaceTabsAction = |
| 30 | | { readonly type: "select"; readonly tab: WorkspaceTab } |
| 31 | | { readonly type: "search"; readonly tab?: WorkspaceTab } |
| 32 | | { readonly type: "query"; readonly query: string; readonly tab?: WorkspaceTab } |
| 33 | | { readonly type: "expand"; readonly tab?: WorkspaceTab } |
| 34 | | { readonly type: "cancel"; readonly tab?: WorkspaceTab } |
| 35 | | { readonly type: "scroll"; readonly tab: WorkspaceTab; readonly offset: number }; |
| 36 | |
| 37 | /** Each root retains its own search; leaving search restores the unfiltered list position. */ |
| 38 | export function workspaceTabsReducer( |
| 39 | state: WorkspaceTabsState, |
| 40 | action: WorkspaceTabsAction, |
| 41 | ): WorkspaceTabsState { |
| 42 | if (action.type === "select") |
| 43 | return action.tab === state.selectedTab |
| 44 | ? state |
| 45 | : { |
| 46 | ...state, |
| 47 | selectedTab: action.tab, |
| 48 | selectionRevision: state.selectionRevision + 1, |
| 49 | }; |
| 50 | const tab = action.tab ?? state.selectedTab; |
| 51 | const current = state.tabs[tab]; |
| 52 | let next: WorkspaceTabState; |
| 53 | switch (action.type) { |
| 54 | case "search": |
| 55 | next = { |
| 56 | ...current, |
| 57 | searchMode: "active", |
| 58 | searchEntryOffset: |
| 59 | current.searchMode === "resting" ? current.scrollOffset : current.searchEntryOffset, |
| 60 | }; |
| 61 | break; |
| 62 | case "query": |
| 63 | next = { ...current, query: action.query }; |
| 64 | break; |
| 65 | case "expand": |
| 66 | if (current.searchMode === "resting") return state; |
| 67 | next = { ...current, searchMode: "expanded" }; |
| 68 | break; |
| 69 | case "cancel": |
| 70 | next = { |
| 71 | ...current, |
| 72 | query: "", |
| 73 | searchMode: "resting", |
| 74 | scrollOffset: current.searchEntryOffset, |
| 75 | }; |
| 76 | break; |
| 77 | case "scroll": |
| 78 | if (current.scrollOffset === action.offset) return state; |
| 79 | next = { ...current, scrollOffset: action.offset }; |
| 80 | break; |
| 81 | } |
| 82 | return { ...state, tabs: { ...state.tabs, [tab]: next } }; |
| 83 | } |
| 84 | |
| 85 | export const WORKSPACE_TOOLBAR = { height: 48, expandedGap: 10, edge: 16, gap: 8 }; |
| 86 | export const WORKSPACE_DOCK = { height: 48, tabsWidth: 176, gap: 12, bottomGap: 6 }; |
| 87 | |
| 88 | export function workspaceToolbarHeight(mode: WorkspaceSearchMode): number { |
| 89 | return mode === "expanded" |
| 90 | ? WORKSPACE_TOOLBAR.height * 2 + WORKSPACE_TOOLBAR.expandedGap |
| 91 | : WORKSPACE_TOOLBAR.height; |
| 92 | } |