Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
</script>

<script lang="ts">
import GlobalThemeRadio from "./GlobalThemeRadio.svelte";
import SettingsPopoverGroup from "./SettingsPopoverGroup.svelte";
import type { RestProps } from "$lib/types";
import { mergeProps, Popover, type WithChildren } from "bits-ui";
import SimpleRadioGroup from "$lib/components/settings-popover/SimpleRadioGroup.svelte";
import { getGlobalTheme, setGlobalTheme } from "$lib/theme.svelte";

let { children, ...restProps }: WithChildren<RestProps> = $props();

Expand All @@ -25,7 +26,7 @@
{#snippet globalThemeSetting()}
<SettingsPopoverGroup title="Theme">
<div class="px-2 py-1">
<GlobalThemeRadio aria-label="Select theme" />
<SimpleRadioGroup values={["light", "dark", "auto"]} bind:value={getGlobalTheme, setGlobalTheme} aria-label="Select theme" />
</div>
</SettingsPopoverGroup>
{/snippet}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
<script lang="ts">
import { getGlobalTheme, setGlobalTheme } from "$lib/theme.svelte";
import { Label, RadioGroup, useId } from "bits-ui";
import { Label, RadioGroup } from "bits-ui";
import { capitalizeFirstLetter } from "$lib/util";
import type { RestProps } from "$lib/types";

let { ...props } = $props();
interface Props extends RestProps {
value: string;
values: string[];
}

let { value = $bindable(), values, ...restProps }: Props = $props();

const uid = $props.id();
</script>

{#snippet themeItem(theme: string)}
{@const labelId = useId()}
{@const itemId = useId()}
{#snippet item(value: string)}
{@const labelId = `${uid}-${value}-label`}
{@const itemId = `${uid}-${value}-item`}
<Label.Root id={labelId} for={itemId} class="flex cursor-pointer flex-row items-center gap-1 text-sm">
<RadioGroup.Item
class="flex size-4 cursor-pointer items-center justify-center rounded-full border btn-ghost"
value={theme}
{value}
id={itemId}
aria-labelledby={labelId}
>
Expand All @@ -22,12 +29,12 @@
{/if}
{/snippet}
</RadioGroup.Item>
{capitalizeFirstLetter(theme)}
{capitalizeFirstLetter(value)}
</Label.Root>
{/snippet}

<RadioGroup.Root class="flex flex-row items-center gap-2" bind:value={getGlobalTheme, setGlobalTheme} {...props}>
{@render themeItem("light")}
{@render themeItem("dark")}
{@render themeItem("auto")}
<RadioGroup.Root class="flex flex-row items-center gap-2" bind:value {...restProps}>
{#each values as value (value)}
{@render item(value)}
{/each}
</RadioGroup.Root>
8 changes: 8 additions & 0 deletions web/src/lib/diff-viewer-multi-file.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { VList } from "virtua/svelte";
import { Context, Debounced } from "runed";
import { MediaQuery } from "svelte/reactivity";

export type SidebarLocation = "left" | "right";

export class GlobalOptions {
private static readonly localStorageKey = "diff-viewer-global-options";
private static readonly context = new Context<GlobalOptions>(GlobalOptions.localStorageKey);
Expand Down Expand Up @@ -54,6 +56,8 @@ export class GlobalOptions {
wordDiffs = $state(true);
lineWrap = $state(true);
omitPatchHeaderOnlyHunks = $state(true);
// TODO: send to server (use cookie?) to that the initial position is correct
sidebarLocation: SidebarLocation = $state("left");

private constructor() {
$effect(() => {
Expand Down Expand Up @@ -90,6 +94,7 @@ export class GlobalOptions {
omitPatchHeaderOnlyHunks: this.omitPatchHeaderOnlyHunks,
wordDiff: this.wordDiffs,
lineWrap: this.lineWrap,
sidebarLocation: this.sidebarLocation,
};
if (this.syntaxHighlightingThemeLight !== DEFAULT_THEME_LIGHT) {
cereal.syntaxHighlightingThemeLight = this.syntaxHighlightingThemeLight;
Expand Down Expand Up @@ -124,6 +129,9 @@ export class GlobalOptions {
if (jsonObject.lineWrap !== undefined) {
this.lineWrap = jsonObject.lineWrap;
}
if (jsonObject.sidebarLocation !== undefined) {
this.sidebarLocation = jsonObject.sidebarLocation;
}
}
}

Expand Down
55 changes: 29 additions & 26 deletions web/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import SettingsPopoverGroup from "$lib/components/settings-popover/SettingsPopoverGroup.svelte";
import LabeledCheckbox from "$lib/components/LabeledCheckbox.svelte";
import ShikiThemeSelector from "$lib/components/settings-popover/ShikiThemeSelector.svelte";
import SimpleRadioGroup from "$lib/components/settings-popover/SimpleRadioGroup.svelte";
import DiffSearch from "./DiffSearch.svelte";
import FileHeader from "./FileHeader.svelte";
import DiffTitle from "./DiffTitle.svelte";
Expand All @@ -28,16 +29,21 @@
import ActionsPopover from "./ActionsPopover.svelte";
import LoadDiffDialog from "./LoadDiffDialog.svelte";
import InfoPopup from "./InfoPopup.svelte";
import { Button } from "bits-ui";
import { Button, Label } from "bits-ui";
import { onClickOutside } from "runed";
import SidebarToggle from "./SidebarToggle.svelte";

const globalOptions = GlobalOptions.init();
const viewer = MultiFileDiffViewerState.init();
let sidebarElement: HTMLDivElement | undefined = $state();

onClickOutside(
() => sidebarElement,
() => {
(e) => {
if (e.target instanceof HTMLElement && e.target.closest("[data-sidebar-toggle]")) {
// Ignore toggle button clicks
return;
}
if (!staticSidebar.current) {
viewer.sidebarCollapsed = true;
}
Expand Down Expand Up @@ -104,21 +110,6 @@
/>
</svelte:head>

{#snippet sidebarToggle()}
<button
title={viewer.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
type="button"
class="flex size-6 items-center justify-center rounded-md btn-ghost text-primary"
onclick={() => (viewer.sidebarCollapsed = !viewer.sidebarCollapsed)}
>
{#if viewer.sidebarCollapsed}
<span class="iconify size-4 shrink-0 octicon--sidebar-collapse-16" aria-hidden="true"></span>
{:else}
<span class="iconify size-4 shrink-0 octicon--sidebar-expand-16" aria-hidden="true"></span>
{/if}
</button>
{/snippet}

{#snippet settingsPopover()}
<SettingsPopover class="self-center">
{@render globalThemeSetting()}
Expand All @@ -131,14 +122,28 @@
<LabeledCheckbox labelText="Concise nested diffs" bind:checked={globalOptions.omitPatchHeaderOnlyHunks} />
<LabeledCheckbox labelText="Word diffs" bind:checked={globalOptions.wordDiffs} />
<LabeledCheckbox labelText="Line wrapping" bind:checked={globalOptions.lineWrap} />
<div class="flex justify-between px-2 py-1">
<Label.Root id="sidebarLocationLabel" for="sidebarLocation">Sidebar location</Label.Root>
<SimpleRadioGroup
id="sidebarLocation"
aria-labelledby="sidebarLocationLabel"
values={["left", "right"]}
bind:value={globalOptions.sidebarLocation}
/>
</div>
</SettingsPopoverGroup>
</SettingsPopover>
{/snippet}

<div class="relative flex min-h-screen flex-row justify-center">
<div
bind:this={sidebarElement}
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"
class="absolute top-0 z-10 flex h-full w-full flex-col bg-neutral
data-[collapsed=true]:hidden
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
md:w-[350px] md:shadow-md lg:static
lg:h-auto lg:shadow-none"
data-side={globalOptions.sidebarLocation}
data-collapsed={viewer.sidebarCollapsed}
>
<div class="m-2 flex flex-row items-center gap-2">
Expand All @@ -159,9 +164,7 @@
></button>
{/if}
</div>
<div class="flex items-center lg:hidden">
{@render sidebarToggle()}
</div>
<SidebarToggle class="lg:hidden" />
</div>
{#if viewer.filteredFileDetails.length !== viewer.fileDetails.length}
<div class="ms-2 mb-2 text-sm text-gray-600">
Expand Down Expand Up @@ -233,8 +236,8 @@
</div>
</div>
</div>
<div class="flex grow flex-col p-3">
<div class="mb-2 flex flex-wrap items-center gap-2">
<div class="flex grow flex-col">
<div class="flex flex-wrap items-center gap-2 px-3 py-2">
{#if viewer.diffMetadata !== null}
<DiffTitle meta={viewer.diffMetadata} />
{/if}
Expand Down Expand Up @@ -276,16 +279,16 @@
</InfoPopup>
</div>
</div>
<div class="mb-1 flex flex-row items-center gap-2">
{@render sidebarToggle()}
<div class="flex flex-row items-center gap-2 px-3 pb-2">
<SidebarToggle class="data-[side=right]:order-10" />
{#await viewer.stats}
<DiffStats />
{:then stats}
<DiffStats add={stats.addedLines} remove={stats.removedLines} />
{/await}
<DiffSearch />
</div>
<div class="flex flex-1 flex-col border">
<div class="flex flex-1 flex-col border-t">
<VList data={viewer.fileDetails} style="height: 100%;" getKey={(_, i) => i} bind:this={viewer.vlist} overscan={3}>
{#snippet children(value, index)}
{@const lines = viewer.diffText[index] !== undefined ? viewer.diffText[index] : null}
Expand Down
35 changes: 35 additions & 0 deletions web/src/routes/SidebarToggle.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
import { GlobalOptions, MultiFileDiffViewerState } from "$lib/diff-viewer-multi-file.svelte";
import { Button, mergeProps } from "bits-ui";
import { type RestProps } from "$lib/types";

let { ...restProps }: RestProps = $props();

const viewer = MultiFileDiffViewerState.get();
const globalOptions = GlobalOptions.get();

let mergedProps = $derived(
mergeProps(
{
class: "flex size-6 items-center justify-center rounded-md btn-ghost text-primary",
},
restProps,
),
);
</script>

<Button.Root
title={viewer.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
type="button"
data-side={globalOptions.sidebarLocation}
onclick={() => (viewer.sidebarCollapsed = !viewer.sidebarCollapsed)}
data-sidebar-toggle
{...mergedProps}
>
<span
class="iconify size-4 shrink-0 octicon--sidebar-collapse-16 data-[collapsed=false]:octicon--sidebar-expand-16 data-[side=right]:scale-x-[-1]"
aria-hidden="true"
data-collapsed={viewer.sidebarCollapsed}
data-side={globalOptions.sidebarLocation}
></span>
</Button.Root>