|
20 | 20 | import SettingsPopoverGroup from "$lib/components/settings-popover/SettingsPopoverGroup.svelte"; |
21 | 21 | import LabeledCheckbox from "$lib/components/LabeledCheckbox.svelte"; |
22 | 22 | import ShikiThemeSelector from "$lib/components/settings-popover/ShikiThemeSelector.svelte"; |
| 23 | + import SimpleRadioGroup from "$lib/components/settings-popover/SimpleRadioGroup.svelte"; |
23 | 24 | import DiffSearch from "./DiffSearch.svelte"; |
24 | 25 | import FileHeader from "./FileHeader.svelte"; |
25 | 26 | import DiffTitle from "./DiffTitle.svelte"; |
|
28 | 29 | import ActionsPopover from "./ActionsPopover.svelte"; |
29 | 30 | import LoadDiffDialog from "./LoadDiffDialog.svelte"; |
30 | 31 | import InfoPopup from "./InfoPopup.svelte"; |
31 | | - import { Button } from "bits-ui"; |
| 32 | + import { Button, Label } from "bits-ui"; |
32 | 33 | import { onClickOutside } from "runed"; |
| 34 | + import SidebarToggle from "./SidebarToggle.svelte"; |
33 | 35 |
|
34 | 36 | const globalOptions = GlobalOptions.init(); |
35 | 37 | const viewer = MultiFileDiffViewerState.init(); |
36 | 38 | let sidebarElement: HTMLDivElement | undefined = $state(); |
37 | 39 |
|
38 | 40 | onClickOutside( |
39 | 41 | () => sidebarElement, |
40 | | - () => { |
| 42 | + (e) => { |
| 43 | + if (e.target instanceof HTMLElement && e.target.closest("[data-sidebar-toggle]")) { |
| 44 | + // Ignore toggle button clicks |
| 45 | + return; |
| 46 | + } |
41 | 47 | if (!staticSidebar.current) { |
42 | 48 | viewer.sidebarCollapsed = true; |
43 | 49 | } |
|
104 | 110 | /> |
105 | 111 | </svelte:head> |
106 | 112 |
|
107 | | -{#snippet sidebarToggle()} |
108 | | - <button |
109 | | - title={viewer.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} |
110 | | - type="button" |
111 | | - class="flex size-6 items-center justify-center rounded-md btn-ghost text-primary" |
112 | | - onclick={() => (viewer.sidebarCollapsed = !viewer.sidebarCollapsed)} |
113 | | - > |
114 | | - {#if viewer.sidebarCollapsed} |
115 | | - <span class="iconify size-4 shrink-0 octicon--sidebar-collapse-16" aria-hidden="true"></span> |
116 | | - {:else} |
117 | | - <span class="iconify size-4 shrink-0 octicon--sidebar-expand-16" aria-hidden="true"></span> |
118 | | - {/if} |
119 | | - </button> |
120 | | -{/snippet} |
121 | | - |
122 | 113 | {#snippet settingsPopover()} |
123 | 114 | <SettingsPopover class="self-center"> |
124 | 115 | {@render globalThemeSetting()} |
|
131 | 122 | <LabeledCheckbox labelText="Concise nested diffs" bind:checked={globalOptions.omitPatchHeaderOnlyHunks} /> |
132 | 123 | <LabeledCheckbox labelText="Word diffs" bind:checked={globalOptions.wordDiffs} /> |
133 | 124 | <LabeledCheckbox labelText="Line wrapping" bind:checked={globalOptions.lineWrap} /> |
| 125 | + <div class="flex justify-between px-2 py-1"> |
| 126 | + <Label.Root id="sidebarLocationLabel" for="sidebarLocation">Sidebar location</Label.Root> |
| 127 | + <SimpleRadioGroup |
| 128 | + id="sidebarLocation" |
| 129 | + aria-labelledby="sidebarLocationLabel" |
| 130 | + values={["left", "right"]} |
| 131 | + bind:value={globalOptions.sidebarLocation} |
| 132 | + /> |
| 133 | + </div> |
134 | 134 | </SettingsPopoverGroup> |
135 | 135 | </SettingsPopover> |
136 | 136 | {/snippet} |
137 | 137 |
|
138 | 138 | <div class="relative flex min-h-screen flex-row justify-center"> |
139 | 139 | <div |
140 | 140 | bind:this={sidebarElement} |
141 | | - class="absolute top-0 left-0 z-10 flex h-full w-full flex-col border-e bg-neutral data-[collapsed=true]:hidden md:w-[350px] md:shadow-md lg:static lg:h-auto lg:shadow-none" |
| 141 | + class="absolute top-0 z-10 flex h-full w-full flex-col bg-neutral |
| 142 | + data-[collapsed=true]:hidden |
| 143 | + data-[side=left]:left-0 data-[side=left]:border-e data-[side=right]:right-0 data-[side=right]:order-10 data-[side=right]:border-s |
| 144 | + md:w-[350px] md:shadow-md lg:static |
| 145 | + lg:h-auto lg:shadow-none" |
| 146 | + data-side={globalOptions.sidebarLocation} |
142 | 147 | data-collapsed={viewer.sidebarCollapsed} |
143 | 148 | > |
144 | 149 | <div class="m-2 flex flex-row items-center gap-2"> |
|
159 | 164 | ></button> |
160 | 165 | {/if} |
161 | 166 | </div> |
162 | | - <div class="flex items-center lg:hidden"> |
163 | | - {@render sidebarToggle()} |
164 | | - </div> |
| 167 | + <SidebarToggle class="lg:hidden" /> |
165 | 168 | </div> |
166 | 169 | {#if viewer.filteredFileDetails.length !== viewer.fileDetails.length} |
167 | 170 | <div class="ms-2 mb-2 text-sm text-gray-600"> |
|
233 | 236 | </div> |
234 | 237 | </div> |
235 | 238 | </div> |
236 | | - <div class="flex grow flex-col p-3"> |
237 | | - <div class="mb-2 flex flex-wrap items-center gap-2"> |
| 239 | + <div class="flex grow flex-col"> |
| 240 | + <div class="flex flex-wrap items-center gap-2 px-3 py-2"> |
238 | 241 | {#if viewer.diffMetadata !== null} |
239 | 242 | <DiffTitle meta={viewer.diffMetadata} /> |
240 | 243 | {/if} |
|
276 | 279 | </InfoPopup> |
277 | 280 | </div> |
278 | 281 | </div> |
279 | | - <div class="mb-1 flex flex-row items-center gap-2"> |
280 | | - {@render sidebarToggle()} |
| 282 | + <div class="flex flex-row items-center gap-2 px-3 pb-2"> |
| 283 | + <SidebarToggle class="data-[side=right]:order-10" /> |
281 | 284 | {#await viewer.stats} |
282 | 285 | <DiffStats /> |
283 | 286 | {:then stats} |
284 | 287 | <DiffStats add={stats.addedLines} remove={stats.removedLines} /> |
285 | 288 | {/await} |
286 | 289 | <DiffSearch /> |
287 | 290 | </div> |
288 | | - <div class="flex flex-1 flex-col border"> |
| 291 | + <div class="flex flex-1 flex-col border-t"> |
289 | 292 | <VList data={viewer.fileDetails} style="height: 100%;" getKey={(_, i) => i} bind:this={viewer.vlist} overscan={3}> |
290 | 293 | {#snippet children(value, index)} |
291 | 294 | {@const lines = viewer.diffText[index] !== undefined ? viewer.diffText[index] : null} |
|
0 commit comments