mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-19 15:41:31 +03:00
Merge pull request #3083 from gitbutlerapp/scrollbar-updates
scrollbar-updates
This commit is contained in:
commit
d5c70c4030
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
27
gitbutler-ui/src/lib/components/VideoTip.svelte
Normal file
27
gitbutler-ui/src/lib/components/VideoTip.svelte
Normal 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>
|
@ -214,7 +214,7 @@
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--clr-theme-container-light),
|
||||
var(--darken-tint-light)
|
||||
var(--darken-tint-extralight)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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;
|
||||
|
BIN
gitbutler-ui/static/video-tips/scrollbar-on-hover.webm
Normal file
BIN
gitbutler-ui/static/video-tips/scrollbar-on-hover.webm
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user