Merge pull request #3083 from gitbutlerapp/scrollbar-updates

scrollbar-updates
This commit is contained in:
Pavel Laptev 2024-03-10 11:55:14 +01:00 committed by GitHub
commit d5c70c4030
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 558 additions and 309 deletions

View File

@ -5,6 +5,7 @@
import CommitDialog from './CommitDialog.svelte';
import DropzoneOverlay from './DropzoneOverlay.svelte';
import PullRequestCard from './PullRequestCard.svelte';
import ScrollableContainer from './ScrollableContainer.svelte';
import UpstreamCommits from './UpstreamCommits.svelte';
import { ButlerAIProvider } from '$lib/backend/aiProviders';
import { Summarizer } from '$lib/backend/summarizer';
@ -44,6 +45,7 @@
RemoteBranchData,
RemoteCommit
} from '$lib/vbranches/types';
export let branch: Branch;
export let isUnapplied = false;
export let project: Project;
@ -63,6 +65,7 @@
const aiGenEnabled = projectAiGenEnabled(project.id);
const aiGenAutoBranchNamingEnabled = projectAiGenAutoBranchNamingEnabled(project.id);
let scrollViewport: HTMLElement;
let rsViewport: HTMLElement;
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
@ -187,177 +190,201 @@
/>
</div>
{:else}
<div class="resizer-wrapper">
<div class="resizer-wrapper" bind:this={scrollViewport}>
<div
class="branch-card"
class="branch-card hide-native-scrollbar"
data-tauri-drag-region
class:target-branch={branch.active && branch.selectedForChanges}
>
<div
bind:this={rsViewport}
style:width={`${laneWidth || $defaultBranchWidthRem}rem`}
class="branch-card__contents"
<ScrollableContainer
wide
padding={{
top: `var(--space-12)`,
bottom: `var(--space-12)`
}}
>
<BranchHeader
{isUnapplied}
{branchController}
{branch}
{base}
bind:isLaneCollapsed
projectId={project.id}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
/>
<PullRequestCard
projectId={project.id}
{branch}
{branchService}
{githubService}
{isUnapplied}
isLaneCollapsed={$isLaneCollapsed}
/>
{#if user?.role == 'admin' && unknownCommits && unknownCommits.length > 0 && !branch.conflicted}
<UpstreamCommits
upstream={upstreamData}
branchId={branch.id}
{project}
{branchController}
{branchCount}
projectId={project.id}
{selectedFiles}
{base}
/>
{/if}
<!-- DROPZONES -->
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
<div
class="branch-card__dropzone-wrapper"
use:dropzone={{
hover: 'move-commit-dz-hover',
active: 'move-commit-dz-active',
accepts: acceptMoveCommit,
onDrop: onCommitDrop,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'cherrypick-dz-hover',
active: 'cherrypick-dz-active',
accepts: acceptCherrypick,
onDrop: onCherrypicked,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'lane-dz-hover',
active: 'lane-dz-active',
accepts: acceptBranchDrop,
onDrop: onBranchDrop,
disabled: isUnapplied
}}
bind:this={rsViewport}
style:width={`${laneWidth || $defaultBranchWidthRem}rem`}
class="branch-card__contents"
>
<BranchHeader
{isUnapplied}
{branchController}
{branch}
{base}
bind:isLaneCollapsed
projectId={project.id}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
/>
<PullRequestCard
projectId={project.id}
{branch}
{branchService}
{githubService}
{isUnapplied}
isLaneCollapsed={$isLaneCollapsed}
/>
{#if user?.role == 'admin' && unknownCommits && unknownCommits.length > 0 && !branch.conflicted}
<UpstreamCommits
upstream={upstreamData}
branchId={branch.id}
{project}
{branchController}
{branchCount}
projectId={project.id}
{selectedFiles}
{base}
/>
{/if}
<!-- DROPZONES -->
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
<DropzoneOverlay class="move-commit-dz-marker" label="Move here" />
{#if branch.files?.length > 0}
<div class="card">
{#if branch.active && branch.conflicted}
<div class="mb-2 bg-red-500 p-2 font-bold text-white">
{#if branch.files.some((f) => f.conflicted)}
This virtual branch conflicts with upstream changes. Please resolve all
conflicts and commit before you can continue.
{:else}
Please commit your resolved conflicts to continue.
{/if}
</div>
{/if}
<BranchFiles
branchId={branch.id}
files={branch.files}
{isUnapplied}
{branchController}
{project}
{selectedOwnership}
{selectedFiles}
showCheckboxes={$commitBoxOpen}
allowMultiple={true}
readonly={false}
/>
{#if branch.active}
<CommitDialog
projectId={project.id}
<div
class="branch-card__dropzone-wrapper"
use:dropzone={{
hover: 'move-commit-dz-hover',
active: 'move-commit-dz-active',
accepts: acceptMoveCommit,
onDrop: onCommitDrop,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'cherrypick-dz-hover',
active: 'cherrypick-dz-active',
accepts: acceptCherrypick,
onDrop: onCherrypicked,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'lane-dz-hover',
active: 'lane-dz-active',
accepts: acceptBranchDrop,
onDrop: onBranchDrop,
disabled: isUnapplied
}}
>
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
<DropzoneOverlay class="move-commit-dz-marker" label="Move here" />
{#if branch.files?.length > 0}
<div class="card">
{#if branch.active && branch.conflicted}
<div class="mb-2 bg-red-500 p-2 font-bold text-white">
{#if branch.files.some((f) => f.conflicted)}
This virtual branch conflicts with upstream changes. Please resolve all
conflicts and commit before you can continue.
{:else}
Please commit your resolved conflicts to continue.
{/if}
</div>
{/if}
<BranchFiles
branchId={branch.id}
files={branch.files}
{isUnapplied}
{branchController}
{branch}
{cloud}
{project}
{selectedOwnership}
{user}
bind:expanded={commitBoxOpen}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
{selectedFiles}
showCheckboxes={$commitBoxOpen}
allowMultiple={true}
readonly={false}
/>
{/if}
</div>
{:else if branch.commits.length == 0}
<div class="new-branch card" data-dnd-ignore>
<div class="new-branch__content">
<div class="new-branch__image">
<ImgThemed
imgSet={{
light: '/images/lane-new-light.webp',
dark: '/images/lane-new-dark.webp'
{#if branch.active}
<CommitDialog
projectId={project.id}
{branchController}
{branch}
{cloud}
{selectedOwnership}
{user}
bind:expanded={commitBoxOpen}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
/>
</div>
<h2 class="new-branch__title text-base-body-15 text-semibold">
This is a new branch.
</h2>
<p class="new-branch__caption text-base-body-13">
You can drag and drop files or parts of files here.
</p>
{/if}
</div>
</div>
{:else}
<!-- attention: these markers have custom css at the bottom of thise file -->
<div class="no-changes card" data-dnd-ignore>
<div class="new-branch__content">
<div class="new-branch__image">
<ImgThemed
imgSet={{
light: '/images/lane-no-changes-light.webp',
dark: '/images/lane-no-changes-dark.webp'
}}
/>
{:else if branch.commits.length == 0}
<div class="new-branch card" data-dnd-ignore>
<div class="new-branch__content">
<div class="new-branch__image">
<ImgThemed
imgSet={{
light: '/images/lane-new-light.webp',
dark: '/images/lane-new-dark.webp'
}}
/>
</div>
<h2 class="new-branch__title text-base-body-15 text-semibold">
This is a new branch.
</h2>
<p class="new-branch__caption text-base-body-13">
You can drag and drop files or parts of files here.
</p>
</div>
<h2 class="new-branch__caption text-base-body-13">
No uncommitted changes<br />on this branch
</h2>
</div>
</div>
{/if}
</div>
{:else}
<!-- attention: these markers have custom css at the bottom of thise file -->
<div class="no-changes card" data-dnd-ignore>
<div class="new-branch__content">
<div class="new-branch__image">
<ImgThemed
imgSet={{
light: '/images/lane-no-changes-light.webp',
dark: '/images/lane-no-changes-dark.webp'
}}
/>
</div>
<h2 class="new-branch__caption text-base-body-13">
No uncommitted changes<br />on this branch
</h2>
</div>
</div>
{/if}
</div>
<BranchCommits
{base}
{branch}
{project}
{githubService}
{branchController}
{branchService}
{branchCount}
{isUnapplied}
{selectedFiles}
<BranchCommits
{base}
{branch}
{project}
{githubService}
{branchController}
{branchService}
{branchCount}
{isUnapplied}
{selectedFiles}
/>
</div>
</ScrollableContainer>
<div class="divider-line">
<Resizer
viewport={rsViewport}
direction="right"
minWidth={320}
sticky
defaultLineColor={$selectedFiles.length > 0
? 'transparent'
: 'var(--clr-theme-container-outline-light)'}
on:width={(e) => {
laneWidth = e.detail / (16 * $userSettings.zoom);
lscache.set(laneWidthKey + branch.id, laneWidth, 7 * 1440); // 7 day ttl
$defaultBranchWidthRem = laneWidth;
}}
/>
</div>
</div>
<div class="divider-line">
<!-- <div class="divider-line">
<Resizer
viewport={rsViewport}
direction="right"
@ -372,7 +399,7 @@
$defaultBranchWidthRem = laneWidth;
}}
/>
</div>
</div> -->
</div>
{/if}
@ -388,19 +415,14 @@
user-select: none;
overflow-x: hidden;
overflow-y: scroll;
&::-webkit-scrollbar {
width: 0px;
background: transparent; /* Chrome/Safari/Webkit */
}
}
.divider-line {
z-index: 30;
position: absolute;
top: 0;
right: 0;
height: 100%;
transform: translateX(var(--selected-resize-shift));
}
.branch-card__dropzone-wrapper {

View File

@ -138,26 +138,17 @@
user-select: none; /* here because of user-select draggable interference in board */
position: relative;
--target-branch-background: var(--clr-theme-container-pale);
--selected-resize-shift: 0;
--selected-target-branch-right-padding: 0;
--selected-opacity: 1;
background-color: var(--target-branch-background);
}
.target-branch {
--target-branch-background: color-mix(
in srgb,
var(--clr-theme-scale-pop-60) 15%,
var(--clr-theme-scale-pop-60) 20%,
var(--clr-theme-container-pale)
);
}
.file-selected {
--selected-resize-shift: calc((var(--space-6) + 0.0625rem) * -1);
--selected-target-branch-right-padding: calc(var(--space-4) * -1);
--selected-opacity: 0;
}
.file-preview {
display: flex;
position: relative;
@ -167,6 +158,5 @@
align-items: self-start;
padding: var(--space-12) var(--space-12) var(--space-12) 0;
margin-left: var(--selected-target-branch-right-padding);
}
</style>

View File

@ -9,12 +9,15 @@
import { storeToObservable } from '$lib/rxjs/store';
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/settings/userSettings';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { createEventDispatcher } from 'svelte';
import { getContext, onDestroy, onMount } from 'svelte';
import { derived } from 'svelte/store';
import type { BranchService } from '$lib/branches/service';
import type { CombinedBranch } from '$lib/branches/types';
import type { GitHubService } from '$lib/github/service';
const dispatch = createEventDispatcher<{ scrollbarDragging: boolean }>();
export let branchService: BranchService;
export let githubService: GitHubService;
export let projectId: string;
@ -151,7 +154,11 @@
/>
</BranchesHeader>
{#if $branches$?.length > 0}
<ScrollableContainer bind:viewport showBorderWhenScrolled>
<ScrollableContainer
bind:viewport
showBorderWhenScrolled
on:dragging={(e) => dispatch('scrollbarDragging', e.detail)}
>
<div class="scroll-container">
<TextBox
icon="filter"

View File

@ -2,6 +2,7 @@
import Button from './Button.svelte';
import HunkContextMenu from './HunkContextMenu.svelte';
import HunkLine from './HunkLine.svelte';
import Scrollbar from './Scrollbar.svelte';
import { draggable } from '$lib/dragging/draggable';
import { draggableHunk } from '$lib/dragging/draggables';
import { onDestroy } from 'svelte';
@ -11,6 +12,9 @@
import type { Hunk } from '$lib/vbranches/types';
import type { Writable } from 'svelte/store';
export let viewport: HTMLDivElement | undefined = undefined;
export let contents: HTMLDivElement | undefined = undefined;
export let filePath: string;
export let section: HunkSection;
export let branchId: string | undefined;
@ -56,54 +60,66 @@
let alwaysShow = false;
</script>
<div
tabindex="0"
role="cell"
use:draggable={{
...draggableHunk(branchId, section.hunk),
disabled: draggingDisabled
}}
on:contextmenu|preventDefault
class="hunk"
class:readonly
class:opacity-60={section.hunk.locked && !isFileLocked}
>
<div class="hunk__bg-stretch">
{#if linesModified > 1000 && !alwaysShow}
<div class="flex flex-col p-1">
Change hidden as large diffs may slow down the UI
<Button kind="outlined" color="neutral" on:click={() => (alwaysShow = true)}
>show anyways</Button
>
</div>
{:else}
{#each section.subSections as subsection}
{@const hunk = section.hunk}
{#each subsection.lines.slice(0, subsection.expanded ? subsection.lines.length : 0) as line}
<HunkLine
{line}
{filePath}
{readonly}
{minWidth}
{selectable}
{draggingDisabled}
selected={$selectedOwnership?.containsHunk(hunk.filePath, hunk.id)}
on:selected={(e) => onHunkSelected(hunk, e.detail)}
sectionType={subsection.sectionType}
on:contextmenu={(e) =>
popupMenu.openByMouse(e, {
hunk,
section: subsection,
lineNumber: line.afterLineNumber ? line.afterLineNumber : line.beforeLineNumber
})}
/>
<div class="scrollable">
<div
bind:this={viewport}
tabindex="0"
role="cell"
use:draggable={{
...draggableHunk(branchId, section.hunk),
disabled: draggingDisabled
}}
on:contextmenu|preventDefault
class="hunk hide-native-scrollbar"
class:readonly
class:opacity-60={section.hunk.locked && !isFileLocked}
>
<div bind:this={contents} class="hunk__bg-stretch">
{#if linesModified > 1000 && !alwaysShow}
<div class="flex flex-col p-1">
Change hidden as large diffs may slow down the UI
<Button kind="outlined" color="neutral" on:click={() => (alwaysShow = true)}
>show anyways</Button
>
</div>
{:else}
{#each section.subSections as subsection}
{@const hunk = section.hunk}
{#each subsection.lines.slice(0, subsection.expanded ? subsection.lines.length : 0) as line}
<HunkLine
{line}
{filePath}
{readonly}
{minWidth}
{selectable}
{draggingDisabled}
selected={$selectedOwnership?.containsHunk(hunk.filePath, hunk.id)}
on:selected={(e) => onHunkSelected(hunk, e.detail)}
sectionType={subsection.sectionType}
on:contextmenu={(e) =>
popupMenu.openByMouse(e, {
hunk,
section: subsection,
lineNumber: line.afterLineNumber ? line.afterLineNumber : line.beforeLineNumber
})}
/>
{/each}
{/each}
{/each}
{/if}
{/if}
</div>
</div>
<Scrollbar {viewport} {contents} horz />
</div>
<style lang="postcss">
.scrollable {
display: flex;
flex-direction: column;
position: relative;
border-radius: var(--radius-s);
overflow: hidden;
}
.hunk {
display: flex;
flex-direction: column;

View File

@ -40,6 +40,7 @@
let viewport: HTMLDivElement;
let isResizerHovered = false;
let isResizerDragging = false;
let isScrollbarDragging = false;
$: isNavCollapsed = persisted<boolean>(false, 'projectNavCollapsed_' + project.id);
@ -56,26 +57,25 @@
);
</script>
<aside class="navigation-wrapper">
<div class="resizer-wrapper" tabindex="0" role="button">
<aside class="navigation-wrapper" class:hide-fold-button={isScrollbarDragging}>
<div
class="resizer-wrapper"
tabindex="0"
role="button"
class:folding-button_folded={$isNavCollapsed}
>
<button
class="folding-button"
class:resizer-hovered={isResizerHovered || isResizerDragging}
on:mousedown={toggleNavCollapse}
class:folding-button_folded={$isNavCollapsed}
>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 8 12"
fill="none"
><path
d="M6,0L0,6l6,6"
transform="translate(1 0)"
<svg viewBox="0 0 7 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6 1L1.81892 9.78026C1.30084 10.8682 1.30084 12.1318 1.81892 13.2197L6 22"
stroke-width="1.5"
stroke-linejoin="round"
/></svg
>
vector-effect="non-scaling-stroke"
/>
</svg>
</button>
<Resizer
{viewport}
@ -141,7 +141,12 @@
</div>
</div>
{#if !$isNavCollapsed}
<Branches projectId={project.id} {branchService} {githubService} />
<Branches
projectId={project.id}
{branchService}
{githubService}
on:scrollbarDragging={(e) => (isScrollbarDragging = e.detail)}
/>
{/if}
<Footer {user} projectId={project.id} isNavCollapsed={$isNavCollapsed} />
{/if}
@ -153,8 +158,9 @@
display: flex;
position: relative;
&:hover {
&:hover:not(.hide-fold-button) {
& .folding-button {
pointer-events: auto;
opacity: 1;
right: calc(var(--space-6) * -1);
}
@ -197,16 +203,18 @@
.folding-button {
z-index: 42;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: calc(var(--space-2) * -1);
right: calc(var(--space-4) * -1);
top: 50%;
transform: translateY(-50%);
width: var(--space-16);
width: 0.875rem;
height: var(--space-36);
padding: var(--space-4);
background: var(--clr-theme-container-light);
border-radius: var(--radius-m);
border: 1px solid var(--clr-theme-container-outline-light);
pointer-events: none;
opacity: 0;
transition:
background-color var(--transition-fast),
@ -215,21 +223,21 @@
right var(--transition-fast);
& svg {
stroke: var(--clr-theme-scale-ntrl-50);
stroke: var(--clr-theme-scale-ntrl-60);
transition: stroke var(--transition-fast);
width: 45%;
}
&:hover {
background-color: color-mix(
in srgb,
var(--clr-theme-container-light),
var(--darken-tint-extralight)
);
border-color: color-mix(
in srgb,
var(--clr-theme-container-outline-light),
var(--darken-tint-dark)
);
& svg {
stroke: var(--clr-theme-scale-ntrl-50);
}
}
}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import Scrollbar from '$lib/components/Scrollbar.svelte';
import { onDestroy, onMount } from 'svelte';
import Scrollbar, { type ScrollbarPadding } from '$lib/components/Scrollbar.svelte';
import { onDestroy, onMount, createEventDispatcher } from 'svelte';
export let viewport: HTMLDivElement | undefined = undefined;
export let contents: HTMLDivElement | undefined = undefined;
@ -13,8 +13,14 @@
export let initiallyVisible = false;
export let showBorderWhenScrolled = false;
export let padding: ScrollbarPadding = {};
export let shift = '0';
export let thickness = '0.563rem';
let observer: ResizeObserver;
const dispatch = createEventDispatcher<{ dragging: boolean }>();
onMount(() => {
observer = new ResizeObserver(() => {
if (viewport && contents) {
@ -47,8 +53,16 @@
<div bind:this={contents} class="contents">
<slot />
</div>
<Scrollbar
{viewport}
{contents}
{initiallyVisible}
{padding}
{shift}
{thickness}
on:dragging={(e) => dispatch('dragging', e.detail)}
/>
</div>
<Scrollbar {viewport} {contents} thickness="0.375rem" {initiallyVisible} />
</div>
<style lang="postcss">

View File

@ -1,16 +1,27 @@
<script lang="ts" context="module">
export type ScrollbarPadding = { top?: string; right?: string; bottom?: string; left?: string };
</script>
<script lang="ts">
import { onDestroy } from 'svelte';
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/settings/userSettings';
import { onDestroy, createEventDispatcher } from 'svelte';
import { getContext } from 'svelte';
const userSettings = getContext(SETTINGS_CONTEXT) as SettingsStore;
export let viewport: Element;
export let contents: Element;
export let hideAfter = 1000;
export let alwaysVisible = false;
export let initiallyVisible = false;
export let margin: { top?: number; right?: number; bottom?: number; left?: number } = {};
export let opacity = '0.2';
export let thickness = '0.625rem';
export let thickness = '0.563rem';
export let padding: ScrollbarPadding = {};
export let shift = '0';
export let horz = false;
// Custom z-index in case of overlapping with other elements
export let zIndex = 20;
$: vert = !horz;
let thumb: Element;
@ -22,30 +33,41 @@
let timer = 0;
let interacted = false;
let isViewportHovered = false;
let isDragging = false;
$: teardownViewport = setupViewport(viewport);
$: teardownThumb = setupThumb(thumb);
$: teardownTrack = setupTrack(track);
$: teardownContents = setupContents(contents);
$: marginTop = margin.top ?? 0;
$: marginBottom = margin.bottom ?? 0;
$: marginRight = margin.right ?? 0;
$: marginLeft = margin.left ?? 0;
$: paddingTop = padding.top ?? '0px';
$: paddingBottom = padding.bottom ?? '0px';
$: paddingRight = padding.right ?? '0px';
$: paddingLeft = padding.left ?? '0px';
$: wholeHeight = viewport?.scrollHeight ?? 0;
$: wholeWidth = viewport?.scrollWidth ?? 0;
$: scrollTop = viewport?.scrollTop ?? 0;
$: scrollLeft = viewport?.scrollLeft ?? 0;
$: trackHeight = viewport?.clientHeight ?? 0 - (marginTop + marginBottom);
$: trackWidth = viewport?.clientHeight ?? 0 - (marginTop + marginBottom);
$: trackHeight = viewport?.clientHeight ?? 0;
$: trackWidth = viewport?.clientHeight ?? 0;
$: thumbHeight = wholeHeight > 0 ? (trackHeight / wholeHeight) * trackHeight : 0;
$: thumbWidth = wholeWidth > 0 ? (trackWidth / wholeWidth) * trackWidth : 0;
$: thumbTop = wholeHeight > 0 ? (scrollTop / wholeHeight) * trackHeight : 0;
$: thumbLeft = wholeHeight > 0 ? (scrollLeft / wholeWidth) * trackWidth : 0;
$: alwaysVisible = $userSettings.scrollbarVisabilityOnHover;
$: scrollableY = wholeHeight > trackHeight;
$: scrollableX = wholeWidth > trackWidth;
$: visible = (scrollableY || scrollableX) && (alwaysVisible || initiallyVisible);
$: visible =
((scrollableY || scrollableX) && initiallyVisible) ||
(alwaysVisible && isViewportHovered && (scrollableY || scrollableX));
const dispatch = createEventDispatcher<{
dragging: boolean;
}>();
function setupViewport(viewport: Element) {
if (!viewport) return;
@ -54,18 +76,25 @@
if (typeof window.ResizeObserver === 'undefined') {
throw new Error('window.ResizeObserver is missing.');
}
const observer = new ResizeObserver((entries) => {
for (const _entry of entries) {
wholeHeight = viewport?.scrollHeight ?? 0;
wholeWidth = viewport?.scrollWidth ?? 0;
trackHeight = viewport?.clientHeight - (marginTop + marginBottom) ?? 0;
trackWidth = viewport?.clientWidth - (marginLeft + marginRight) ?? 0;
trackHeight = viewport?.clientHeight ?? 0;
trackWidth = viewport?.clientWidth;
}
});
observer.observe(viewport);
viewport.addEventListener('scroll', onScroll, { passive: true });
if (alwaysVisible) {
viewport.addEventListener('mouseenter', onViewportMouseEnter);
viewport.addEventListener('mouseleave', onViewportMouseLeave);
}
return () => {
observer.unobserve(contents);
observer.disconnect();
@ -73,6 +102,14 @@
};
}
function onViewportMouseEnter() {
isViewportHovered = true;
}
function onViewportMouseLeave() {
isViewportHovered = false;
}
function setupTrack(track: Element) {
if (!track) return;
teardownTrack?.();
@ -107,6 +144,7 @@
const observer = new ResizeObserver((entries) => {
for (const _entry of entries) {
wholeHeight = viewport?.scrollHeight ?? 0;
wholeWidth = viewport?.scrollWidth ?? 0;
}
});
observer.observe(contents);
@ -120,7 +158,8 @@
function setupTimer() {
timer = window.setTimeout(() => {
visible =
((scrollableY || scrollableX) && (alwaysVisible || (initiallyVisible && !interacted))) ||
((scrollableY || scrollableX) && initiallyVisible && !interacted) ||
(isViewportHovered && alwaysVisible) ||
false;
}, hideAfter);
}
@ -178,6 +217,8 @@
event.stopPropagation();
event.preventDefault();
isDragging = true;
startTop = viewport.scrollTop;
startLeft = viewport.scrollLeft;
if (event instanceof MouseEvent) {
@ -207,6 +248,8 @@
startLeft = 0;
startX = 0;
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
@ -216,26 +259,119 @@
teardownContents?.();
teardownThumb?.();
});
$: {
dispatch('dragging', isDragging);
}
</script>
<div
bind:this={track}
class="absolute top-0 duration-200"
class:right-0={vert}
class:top-0={vert}
class:bottom-0={horz}
class:left-0={horz}
style:width={vert ? thickness : `${trackWidth}px`}
style:height={vert ? `${trackHeight}px` : thickness}
style:margin={`${marginTop}rem ${marginRight}rem ${marginBottom}rem ${marginLeft}rem`}
class="scrollbar-track"
class:horz
class:vert
class:show-scrollbar={visible}
class:thumb-dragging={isDragging}
style:width={vert ? thickness : `100%`}
style:height={vert ? `100%` : thickness}
style:z-index={zIndex}
style="
--scrollbar-shift-vertical: {vert ? '0' : shift};
--scrollbar-shift-horizontal: {horz ? '0' : shift};
"
>
<div
bind:this={thumb}
class="absolute z-30 bg-black transition-opacity dark:bg-white"
style:opacity={visible ? opacity : 0}
style:left={vert ? undefined : `${thumbLeft}px`}
style:top={vert ? `${thumbTop}px` : undefined}
style:width={vert ? thickness : `${thumbWidth}px`}
style:height={vert ? `${thumbHeight}px` : thickness}
class="scrollbar-thumb"
style="
--thumb-width: {vert
? '100%'
: `calc(${thumbWidth.toFixed(0)}px - (${paddingRight} + ${paddingLeft}))`};
--thumb-height: {vert
? `calc(${thumbHeight.toFixed(0)}px - (${paddingBottom} + ${paddingTop}))`
: '100%'};
--thumb-top: {vert ? `calc(${thumbTop.toFixed(0)}px + ${paddingTop})` : 'auto'};
--thumb-left: {vert ? 'auto' : `calc(${thumbLeft.toFixed(0)}px + ${paddingLeft})`};
"
/>
</div>
<style>
.scrollbar-track {
/* scrollbar variables */
--scrollbar-shift-vertical: 0;
--scrollbar-shift-horizontal: 0;
/* variable props */
bottom: var(--scrollbar-shift-vertical);
right: var(--scrollbar-shift-horizontal);
/* other props */
position: absolute;
/* background-color: aqua; */
transition:
opacity 0.2s,
width 0.1s,
height 0.1s;
}
.scrollbar-thumb {
/* variable props */
width: var(--thumb-width);
height: var(--thumb-height);
top: var(--thumb-top);
left: var(--thumb-left);
/* other props */
position: absolute;
z-index: 30;
background-color: var(--clr-theme-scale-ntrl-0);
opacity: 0;
transition:
opacity 0.2s,
transform 0.15s;
}
/* modify vertical scrollbar */
.scrollbar-track.vert {
& .scrollbar-thumb {
transform: scaleX(0.6);
transform-origin: right;
}
}
/* modify horizontal scrollbar */
.scrollbar-track.horz {
& .scrollbar-thumb {
transform: scaleY(0.65);
transform-origin: bottom;
}
}
/* MODIFIERS */
.show-scrollbar {
& .scrollbar-thumb {
opacity: 0.15;
}
}
/* hover state for thumb */
.show-scrollbar:hover,
.thumb-dragging {
& .scrollbar-thumb {
opacity: 0.25;
}
}
.show-scrollbar.vert:hover,
.thumb-dragging.vert {
& .scrollbar-thumb {
transform: scaleY(1);
}
}
.show-scrollbar.horz:hover,
.thumb-dragging.horz {
& .scrollbar-thumb {
transform: scaleX(1);
}
}
</style>

View File

@ -3,6 +3,8 @@
</script>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let orientation: 'row' | 'column' = 'column';
export let extraPadding = false;
export let roundedTop = true;
@ -14,6 +16,9 @@
export let disabled = false;
const SLOTS = $$props.$$slots;
// event for hover
const dispatch = createEventDispatcher<{ hover: boolean }>();
</script>
<label
@ -30,6 +35,8 @@
class:error={background == 'error'}
class:clickable={labelFor !== ''}
class:disabled
on:mouseenter={() => dispatch('hover', true)}
on:mouseleave={() => dispatch('hover', false)}
>
{#if SLOTS.iconSide}
<div class="section-card__icon-side">

View File

@ -135,6 +135,7 @@
border: 1px solid var(--clr-theme-container-outline-light);
background: var(--clr-theme-container-light);
box-shadow: var(--fx-shadow-s);
overflow: hidden;
}
.options__group {

View File

@ -1,7 +1,8 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/settings/userSettings';
import { getContext } from 'svelte';
import type { SettingsStore } from '$lib/settings/userSettings';
export let userSettings: SettingsStore;
const themes = [
{
@ -20,8 +21,6 @@
preview: '/images/theme-previews/system.svg'
}
];
const userSettings = getContext(SETTINGS_CONTEXT) as SettingsStore;
</script>
<fieldset class="cards-group">

View File

@ -0,0 +1,27 @@
<script lang="ts">
export let src: string;
export let playing: boolean = false;
let video: HTMLVideoElement;
$: if (video) {
if (playing) {
video.play();
} else {
video.pause();
}
}
</script>
<video bind:this={video} class="video-tip" {src} controls={false} loop muted playsinline>
<track kind="captions" />
</video>
<style>
.video-tip {
pointer-events: none;
width: 80px;
border-radius: var(--radius-m);
border: 1px solid var(--clr-theme-container-outline-light);
}
</style>

View File

@ -214,7 +214,7 @@
background-color: color-mix(
in srgb,
var(--clr-theme-container-light),
var(--darken-tint-light)
var(--darken-tint-extralight)
);
}
}

View File

@ -15,6 +15,7 @@ export interface Settings {
defaultFileWidth: number;
defaultTreeHeight: number;
zoom: number;
scrollbarVisabilityOnHover: boolean;
}
const defaults: Settings = {
@ -27,7 +28,8 @@ const defaults: Settings = {
defaultFileWidth: 460,
defaultTreeHeight: 100,
stashedBranchesHeight: 150,
zoom: 1
zoom: 1,
scrollbarVisabilityOnHover: false
};
export type SettingsStore = Writable<Settings>;

View File

@ -48,7 +48,7 @@
<button on:mousedown={() => httpsWarningBannerDismissed.set(true)}>Dismiss</button>
</div>
{/if}
<div class="relative h-full flex-grow">
<div class="board-wrapper">
<div class="scroll-viewport hide-native-scrollbar" bind:this={viewport}>
<div class="scroll-contents" bind:this={contents}>
<Board
@ -64,8 +64,8 @@
{githubService}
/>
</div>
<Scrollbar {viewport} {contents} horz zIndex={50} />
</div>
<Scrollbar {viewport} {contents} horz thickness="0.4rem" />
</div>
</div>
@ -78,5 +78,16 @@
.scroll-contents {
display: flex;
height: 100%;
min-width: 100%;
width: fit-content;
}
/* BOARD */
.board-wrapper {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
height: 100%;
}
</style>

View File

@ -11,17 +11,22 @@
import TextBox from '$lib/components/TextBox.svelte';
import ThemeSelector from '$lib/components/ThemeSelector.svelte';
import Toggle from '$lib/components/Toggle.svelte';
import VideoTip from '$lib/components/VideoTip.svelte';
import WelcomeSigninAction from '$lib/components/WelcomeSigninAction.svelte';
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
import ProfileSIdebar from '$lib/components/settings/ProfileSIdebar.svelte';
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/settings/userSettings';
import { copyToClipboard } from '$lib/utils/clipboard';
import * as toasts from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url';
import { invoke } from '@tauri-apps/api/tauri';
import { onMount } from 'svelte';
import { getContext } from 'svelte';
import type { PageData } from './$types';
import { goto } from '$app/navigation';
const userSettings = getContext(SETTINGS_CONTEXT) as SettingsStore;
export let data: PageData;
const { cloud, user$, userService, authService } = data;
@ -42,6 +47,8 @@
let deleteConfirmationModal: Modal;
let scrollbarVisabilityVideoPlaying = false;
$: saving = false;
$: userPicture = $user$?.picture;
@ -179,7 +186,40 @@
<SectionCard>
<svelte:fragment slot="title">Appearance</svelte:fragment>
<ThemeSelector />
<ThemeSelector {userSettings} />
</SectionCard>
<SectionCard
labelFor="hoverScrollbarVisability"
orientation="row"
on:hover={(e) => {
scrollbarVisabilityVideoPlaying = e.detail;
}}
>
<svelte:fragment slot="iconSide">
<VideoTip
src="/video-tips/scrollbar-on-hover.webm"
playing={scrollbarVisabilityVideoPlaying}
/>
</svelte:fragment>
<svelte:fragment slot="title">Dynamic scrollbar visibility on hover</svelte:fragment>
<svelte:fragment slot="body">
When turned on, this feature shows the scrollbar automatically when you hover over the
scroll area, even if you're not actively scrolling. By default, the scrollbar stays hidden
until you start scrolling.
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle
id="hoverScrollbarVisability"
checked={$userSettings.scrollbarVisabilityOnHover}
on:change={() =>
userSettings.update((s) => ({
...s,
scrollbarVisabilityOnHover: !s.scrollbarVisabilityOnHover
}))}
/>
</svelte:fragment>
</SectionCard>
<Spacer />

View File

@ -72,37 +72,6 @@ button {
-webkit-app-region: no-drag;
}
/* SCROLL BAR STYLING */
/* We don't use REM here becasue we don't want
the scrollbar to scale with the font size */
.custom-scrollbar {
/* width */
&::-webkit-scrollbar {
width: 16px;
height: 16px;
}
/* Track */
&::-webkit-scrollbar-track {
background: transaparent;
}
/* Handle */
&::-webkit-scrollbar-thumb {
border-radius: 26px;
background: color-mix(in srgb, var(--clr-theme-scale-ntrl-0) 20%, transparent);
background-clip: content-box;
border: 5px solid rgba(0, 0, 0, 0);
}
/* Handle on hover */
&::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--clr-theme-scale-ntrl-0) 30%, transparent);
background-clip: content-box;
border: 5px solid rgba(0, 0, 0, 0);
}
}
/* scrollbar helpers */
.hide-native-scrollbar {
-ms-overflow-style: none;

Binary file not shown.