Tooltip-refactoring-+-new-component (#4804)

* tooltip component + custom svelte transitions

* update some tooltips

* replace old toogle

* replace old tooltip hook

* remove old tooltip hook

* lint fixes

* design tokens update
This commit is contained in:
Pavel Laptev 2024-09-01 20:30:36 +02:00 committed by GitHub
parent 98c3f5d310
commit c820a33e41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 654 additions and 577 deletions

View File

@ -47,7 +47,7 @@
icon="pr-small" icon="pr-small"
style="success" style="success"
kind="solid" kind="solid"
help="These changes have been integrated upstream, update your workspace to make this lane disappear." tooltip="Changes have been integrated upstream, update your workspace to make this lane disappear."
reversedDirection>Integrated</Button reversedDirection>Integrated</Button
> >
{:else} {:else}
@ -56,7 +56,7 @@
size="tag" size="tag"
icon="virtual-branch-small" icon="virtual-branch-small"
style="neutral" style="neutral"
help="These changes are in your working directory." tooltip="Changes are in your working directory"
reversedDirection>Virtual</Button reversedDirection>Virtual</Button
> >
{/if} {/if}
@ -68,7 +68,7 @@
style="neutral" style="neutral"
shrinkable shrinkable
disabled disabled
help="Branch name that will be used when pushing. You can change it from the lane menu." tooltip={'Branch name that will be used when pushing.\nChange it from the lane menu'}
> >
{name} {name}
</Button> </Button>
@ -81,7 +81,7 @@
style="neutral" style="neutral"
kind="solid" kind="solid"
icon="remote-branch-small" icon="remote-branch-small"
help="At least some of your changes have been pushed" tooltip="Some changes have been pushed"
reversedDirection>Remote</Button reversedDirection>Remote</Button
> >
<Button <Button

View File

@ -132,7 +132,7 @@
<div class="draggable" data-drag-handle> <div class="draggable" data-drag-handle>
<Icon name="draggable" /> <Icon name="draggable" />
</div> </div>
<Button style="ghost" outline icon="unfold-lane" help="Expand lane" onclick={expandLane} /> <Button style="ghost" outline icon="unfold-lane" tooltip="Expand lane" onclick={expandLane} />
</div> </div>
<div class="collapsed-lane__info-wrap" bind:clientHeight={headerInfoHeight}> <div class="collapsed-lane__info-wrap" bind:clientHeight={headerInfoHeight}>
@ -147,7 +147,7 @@
clickable={false} clickable={false}
style="warning" style="warning"
kind="soft" kind="soft"
help="Uncommitted changes" tooltip="Uncommitted changes"
> >
{uncommittedChanges} {uncommittedChanges}
{uncommittedChanges === 1 ? 'change' : 'changes'} {uncommittedChanges === 1 ? 'change' : 'changes'}
@ -198,7 +198,7 @@
clickable={false} clickable={false}
icon="locked-small" icon="locked-small"
style="warning" style="warning"
help="Applying this branch will add merge conflict markers that you will have to resolve" tooltip="Applying this branch will add merge conflict markers that you will have to resolve"
> >
Conflict Conflict
</Button> </Button>
@ -214,7 +214,7 @@
<Button <Button
style="pop" style="pop"
kind="soft" kind="soft"
help="New changes will land here" tooltip="New changes will land here"
icon="target" icon="target"
clickable={false} clickable={false}
> >
@ -224,7 +224,7 @@
<Button <Button
style="ghost" style="ghost"
outline outline
help="When selected, new changes will land here" tooltip="When selected, new changes land here"
icon="target" icon="target"
onclick={async () => { onclick={async () => {
isTargetBranchAnimated = true; isTargetBranchAnimated = true;
@ -242,7 +242,7 @@
<PullRequestButton <PullRequestButton
click={async ({ draft }) => await createPr({ draft })} click={async ({ draft }) => await createPr({ draft })}
disabled={branch.commits.length === 0 || !$gitHost} disabled={branch.commits.length === 0 || !$gitHost}
help={!$gitHost ? 'You can enable git host integration in the settings' : ''} tooltip={!$gitHost ? 'You can enable git host integration in the settings' : ''}
loading={isLoading} loading={isLoading}
/> />
{/if} {/if}

View File

@ -14,6 +14,7 @@
import { VirtualBranch } from '$lib/vbranches/types'; import { VirtualBranch } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte'; import Modal from '@gitbutler/ui/Modal.svelte';
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
interface Props { interface Props {
contextMenuEl?: ContextMenu; contextMenuEl?: ContextMenu;
@ -138,13 +139,9 @@
<ContextMenuSection> <ContextMenuSection>
<ContextMenuItem label="Allow rebasing" on:click={toggleAllowRebasing}> <ContextMenuItem label="Allow rebasing" on:click={toggleAllowRebasing}>
<Toggle <Tooltip slot="control" text={'Allows changing commits after push\n(force push needed)'}>
small <Toggle small bind:checked={allowRebasing} on:click={toggleAllowRebasing} />
slot="control" </Tooltip>
bind:checked={allowRebasing}
on:click={toggleAllowRebasing}
help="Allows changing commits after push (force push needed)"
/>
</ContextMenuItem> </ContextMenuItem>
</ContextMenuSection> </ContextMenuSection>

View File

@ -10,7 +10,7 @@
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import Modal from '@gitbutler/ui/Modal.svelte'; import Modal from '@gitbutler/ui/Modal.svelte';
import { tooltip } from '@gitbutler/ui/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import type { PullRequest } from '$lib/gitHost/interface/types'; import type { PullRequest } from '$lib/gitHost/interface/types';
import type { Branch } from '$lib/vbranches/types'; import type { Branch } from '$lib/vbranches/types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -40,13 +40,18 @@
<BranchLabel disabled name={branch.name} /> <BranchLabel disabled name={branch.name} />
<div class="header__remote-branch"> <div class="header__remote-branch">
{#if remoteBranch} {#if remoteBranch}
<div <Tooltip text="At least some of your changes have been pushed'">
class="status-tag text-11 text-semibold remote" <Button
use:tooltip={'At least some of your changes have been pushed'} size="tag"
icon="remote-branch-small"
style="neutral"
kind="solid"
clickable={false}
> >
<Icon name="remote-branch-small" />
{localBranch ? 'local and remote' : 'remote'} {localBranch ? 'local and remote' : 'remote'}
</div> </Button>
</Tooltip>
{#if gitHostBranch} {#if gitHostBranch}
<Button <Button
size="tag" size="tag"
@ -93,7 +98,7 @@
<Button <Button
style="ghost" style="ghost"
outline outline
help="Restores these changes into your working directory" tooltip="Restores these changes into your working directory"
icon="plus-small" icon="plus-small"
loading={isApplying} loading={isApplying}
disabled={$mode?.type !== 'OpenWorkspace'} disabled={$mode?.type !== 'OpenWorkspace'}
@ -120,7 +125,7 @@
<Button <Button
style="ghost" style="ghost"
outline outline
help="Deletes the local branch. If this branch is also present on a remote, it will not be deleted there." tooltip="Deletes the local branch. If this branch is also present on a remote, it will not be deleted there."
icon="bin-small" icon="bin-small"
loading={isDeleting} loading={isDeleting}
disabled={!localBranch} disabled={!localBranch}

View File

@ -26,8 +26,8 @@
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import Modal from '@gitbutler/ui/Modal.svelte'; import Modal from '@gitbutler/ui/Modal.svelte';
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import { getTimeAgo } from '@gitbutler/ui/utils/timeAgo'; import { getTimeAgo } from '@gitbutler/ui/utils/timeAgo';
import { tooltip } from '@gitbutler/ui/utils/tooltip';
import { type Snippet } from 'svelte'; import { type Snippet } from 'svelte';
export let branch: VirtualBranch | undefined = undefined; export let branch: VirtualBranch | undefined = undefined;
@ -299,24 +299,25 @@
<div class="text-11 commit__subtitle"> <div class="text-11 commit__subtitle">
{#if commit.isSigned} {#if commit.isSigned}
<div class="commit__signed" use:tooltip={{ text: 'Signed', delay: 500 }}> <Tooltip text="Signed">
<div class="commit__signed">
<Icon name="success-outline-small" /> <Icon name="success-outline-small" />
</div> </div>
</Tooltip>
<span class="commit__subtitle-divider"></span> <span class="commit__subtitle-divider"></span>
{/if} {/if}
{#if conflicted} {#if conflicted}
<div <Tooltip
class="commit__conflicted" text={"Conflicted commits must be resolved before they can be ammended or squashed.\nPlease resolve conflicts using the 'Resolve conflicts' button"}
use:tooltip={{
text: 'Conflicted commits must be resolved before they can be ammended or squashed.\n\nPlease resolve conflicts using the "Resolve conflicts" button'
}}
> >
<div class="commit__conflicted">
<Icon name="warning-small" /> <Icon name="warning-small" />
Conflicted Conflicted
</div> </div>
</Tooltip>
<span class="commit__subtitle-divider"></span> <span class="commit__subtitle-divider"></span>
{/if} {/if}
@ -593,6 +594,7 @@
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
transition: color var(--transition-fast);
&:hover { &:hover {
color: var(--clr-text-1); color: var(--clr-text-1);

View File

@ -3,11 +3,11 @@
import { persistedCommitMessage, projectRunCommitHooks } from '$lib/config/config'; import { persistedCommitMessage, projectRunCommitHooks } from '$lib/config/config';
import { getContext, getContextStore } from '$lib/utils/context'; import { getContext, getContextStore } from '$lib/utils/context';
import { intersectionObserver } from '$lib/utils/intersectionObserver'; import { intersectionObserver } from '$lib/utils/intersectionObserver';
import { slideFade } from '$lib/utils/svelteTransitions';
import { BranchController } from '$lib/vbranches/branchController'; import { BranchController } from '$lib/vbranches/branchController';
import { Ownership } from '$lib/vbranches/ownership'; import { Ownership } from '$lib/vbranches/ownership';
import { VirtualBranch } from '$lib/vbranches/types'; import { VirtualBranch } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import { slideFade } from '@gitbutler/ui/utils/transitions';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
export let projectId: string; export let projectId: string;

View File

@ -228,7 +228,7 @@
wide wide
loading={isPushingCommits} loading={isPushingCommits}
disabled={localCommitsConflicted} disabled={localCommitsConflicted}
help={localCommitsConflicted tooltip={localCommitsConflicted
? 'In order to push, please resolve any conflicted commits.' ? 'In order to push, please resolve any conflicted commits.'
: undefined} : undefined}
onclick={async () => { onclick={async () => {

View File

@ -21,7 +21,7 @@
import { VirtualBranch, LocalFile } from '$lib/vbranches/types'; import { VirtualBranch, LocalFile } from '$lib/vbranches/types';
import Checkbox from '@gitbutler/ui/Checkbox.svelte'; import Checkbox from '@gitbutler/ui/Checkbox.svelte';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import { tooltip } from '@gitbutler/ui/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@ -179,27 +179,21 @@
{/if} {/if}
{#if title.length > 50} {#if title.length > 50}
<div <Tooltip text={'50 characters or less is best.\nUse description for more details'}>
transition:fly={{ y: 2, duration: 150 }} <div transition:fly={{ y: 2, duration: 150 }} class="commit-box__textarea-tooltip">
class="commit-box__textarea-tooltip"
use:tooltip={{
text: '50 characters or less is best. Extra info can be added in the description.',
delay: 200
}}
>
<Icon name="idea" /> <Icon name="idea" />
</div> </div>
</Tooltip>
{/if} {/if}
<div <Tooltip
class="commit-box__texarea-actions" text={!aiConfigurationValid
class:commit-box-actions_expanded={isExpanded} ? 'You must be logged in or have provided your own API key'
use:tooltip={!aiConfigurationValid
? 'You must be logged in or have provided your own API key to use this feature'
: !$aiGenEnabled : !$aiGenEnabled
? 'You must have summary generation enabled to use this feature' ? 'You must have summary generation enabled'
: ''} : undefined}
> >
<div class="commit-box__texarea-actions" class:commit-box-actions_expanded={isExpanded}>
<DropDownButton <DropDownButton
style="ghost" style="ghost"
outline outline
@ -230,6 +224,7 @@
{/snippet} {/snippet}
</DropDownButton> </DropDownButton>
</div> </div>
</Tooltip>
</div> </div>
{/if} {/if}

View File

@ -10,7 +10,7 @@
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Checkbox from '@gitbutler/ui/Checkbox.svelte'; import Checkbox from '@gitbutler/ui/Checkbox.svelte';
import Modal from '@gitbutler/ui/Modal.svelte'; import Modal from '@gitbutler/ui/Modal.svelte';
import { tooltip } from '@gitbutler/ui/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import type { BaseBranch } from '$lib/baseBranch/baseBranch'; import type { BaseBranch } from '$lib/baseBranch/baseBranch';
export let base: BaseBranch; export let base: BaseBranch;
@ -49,7 +49,7 @@
<Button <Button
style="pop" style="pop"
kind="solid" kind="solid"
help={`Merges the commits from ${base.branchName} into the base of all applied virtual branches`} tooltip={`Merges the commits from ${base.branchName} into the base of all applied virtual branches`}
disabled={$mode?.type !== 'OpenWorkspace'} disabled={$mode?.type !== 'OpenWorkspace'}
onclick={() => { onclick={() => {
if ($mergeUpstreamWarningDismissed) { if ($mergeUpstreamWarningDismissed) {
@ -77,12 +77,9 @@
{/if} {/if}
<div> <div>
<h1 <Tooltip text="Current base for virtual branches.">
class="text-13 info-text text-bold" <h1 class="text-13 info-text text-bold">Local</h1>
use:tooltip={'This is the current base for your virtual branches.'} </Tooltip>
>
Local
</h1>
{#each base.recentCommits as commit, index} {#each base.recentCommits as commit, index}
<CommitCard <CommitCard
{commit} {commit}

View File

@ -23,7 +23,7 @@
icon="plus-small" icon="plus-small"
size="tag" size="tag"
width={26} width={26}
help="Insert empty commit" tooltip="Insert empty commit"
helpShowDelay={500} helpShowDelay={500}
onclick={() => dispatch('click')} onclick={() => dispatch('click')}
/> />

View File

@ -138,7 +138,7 @@
<Button <Button
style="pop" style="pop"
kind="solid" kind="solid"
help="Does not create a commit. Can be toggled." tooltip="Does not create a commit. Can be toggled."
onclick={async () => createRemoteModal?.show()}>Apply from fork</Button onclick={async () => createRemoteModal?.show()}>Apply from fork</Button
> >
</div> </div>

View File

@ -24,7 +24,7 @@
style="ghost" style="ghost"
outline outline
icon="update-small" icon="update-small"
help="Last fetch from upstream" tooltip="Last fetch from upstream"
{loading} {loading}
onmousedown={async (e) => { onmousedown={async (e) => {
e.preventDefault(); e.preventDefault();

View File

@ -13,7 +13,7 @@
size="tag" size="tag"
style="error" style="error"
kind="solid" kind="solid"
help="Merge upstream commits into common base" tooltip="Merge upstream into common base"
onclick={async () => { onclick={async () => {
loading = true; loading = true;
try { try {

View File

@ -35,7 +35,7 @@
clickable={false} clickable={false}
icon="locked-small" icon="locked-small"
style="warning" style="warning"
help="File changes cannot be moved because part of this file was already committed into this branch" tooltip="File changes cannot be moved because part of this file was already committed into this branch"
>Locked</Button >Locked</Button
> >
{/if} {/if}

View File

@ -5,7 +5,7 @@
import { getLocalCommits, getLocalAndRemoteCommits } from '$lib/vbranches/contexts'; import { getLocalCommits, getLocalAndRemoteCommits } from '$lib/vbranches/contexts';
import { getLockText } from '$lib/vbranches/tooltip'; import { getLockText } from '$lib/vbranches/tooltip';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import { tooltip } from '@gitbutler/ui/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import type { HunkSection, ContentSection } from '$lib/utils/fileSections'; import type { HunkSection, ContentSection } from '$lib/utils/fileSections';
interface Props { interface Props {
@ -76,14 +76,9 @@
</div> </div>
{#if section.hunk.lockedTo && section.hunk.lockedTo.length > 0 && commits} {#if section.hunk.lockedTo && section.hunk.lockedTo.length > 0 && commits}
<div <Tooltip text={getLockText(section.hunk.lockedTo, commits)}>
use:tooltip={{
text: getLockText(section.hunk.lockedTo, commits),
delay: 500
}}
>
<Icon name="locked-small" color="warning" /> <Icon name="locked-small" color="warning" />
</div> </Tooltip>
{/if} {/if}
{#if section.hunk.poisoned} {#if section.hunk.poisoned}
Can not manage this hunk because it depends on changes from multiple branches Can not manage this hunk because it depends on changes from multiple branches

View File

@ -172,7 +172,7 @@
size="tag" size="tag"
style="ghost" style="ghost"
outline outline
help="Restores GitButler and your files to the state before this operation. Revert actions can also be undone." tooltip="Restores GitButler and your files to the state before this operation. Revert actions can also be undone."
onclick={() => { onclick={() => {
dispatch('restoreClick'); dispatch('restoreClick');
}} }}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { tooltip } from '@gitbutler/ui/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import { type Snippet } from 'svelte'; import { type Snippet } from 'svelte';
interface Props { interface Props {
@ -21,16 +21,17 @@
}: Props = $props(); }: Props = $props();
</script> </script>
<button <Tooltip text={isNavCollapsed ? tooltipLabel : ''} align="start">
use:tooltip={isNavCollapsed ? tooltipLabel : ''} <button
{onmousedown} {onmousedown}
class="domain-button text-14 text-semibold" class="domain-button text-14 text-semibold"
class:selected={isSelected} class:selected={isSelected}
class:align-center={alignItems === 'center'} class:align-center={alignItems === 'center'}
class:align-top={alignItems === 'top'} class:align-top={alignItems === 'top'}
> >
{@render children()} {@render children()}
</button> </button>
</Tooltip>
<style lang="postcss"> <style lang="postcss">
.domain-button { .domain-button {

View File

@ -19,7 +19,9 @@
icon="mail" icon="mail"
style="ghost" style="ghost"
size="cta" size="cta"
help="Share feedback" tooltip="Share feedback"
tooltipAlign="start"
tooltipPosition={isNavCollapsed ? 'bottom' : 'top'}
onclick={() => events.emit('openSendIssueModal')} onclick={() => events.emit('openSendIssueModal')}
wide={isNavCollapsed} wide={isNavCollapsed}
/> />
@ -27,7 +29,9 @@
icon="settings" icon="settings"
style="ghost" style="ghost"
size="cta" size="cta"
help="Project settings" tooltip="Project settings"
tooltipAlign={isNavCollapsed ? 'start' : 'center'}
tooltipPosition={isNavCollapsed ? 'bottom' : 'top'}
onclick={async () => await goto(`/${projectId}/settings`)} onclick={async () => await goto(`/${projectId}/settings`)}
wide={isNavCollapsed} wide={isNavCollapsed}
disabled={$mode?.type !== 'OpenWorkspace'} disabled={$mode?.type !== 'OpenWorkspace'}
@ -36,7 +40,9 @@
icon="timeline" icon="timeline"
style="ghost" style="ghost"
size="cta" size="cta"
help="Project history" tooltip="Project history"
tooltipAlign={isNavCollapsed ? 'start' : 'center'}
tooltipPosition={isNavCollapsed ? 'bottom' : 'top'}
onclick={() => events.emit('openHistory')} onclick={() => events.emit('openHistory')}
wide={isNavCollapsed} wide={isNavCollapsed}
/> />

View File

@ -4,7 +4,7 @@
import ProjectAvatar from '$lib/navigation/ProjectAvatar.svelte'; import ProjectAvatar from '$lib/navigation/ProjectAvatar.svelte';
import { getContext } from '$lib/utils/context'; import { getContext } from '$lib/utils/context';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import { tooltip } from '@gitbutler/ui/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
export let isNavCollapsed: boolean; export let isNavCollapsed: boolean;
@ -15,10 +15,10 @@
</script> </script>
<div class="wrapper"> <div class="wrapper">
<Tooltip text={isNavCollapsed ? project?.title : ''} align="start">
<button <button
bind:this={buttonTrigger} bind:this={buttonTrigger}
class="text-input button" class="text-input button"
use:tooltip={isNavCollapsed ? project?.title : ''}
on:mousedown={(e) => { on:mousedown={(e) => {
e.preventDefault(); e.preventDefault();
popup.toggle(); popup.toggle();
@ -32,6 +32,7 @@
</div> </div>
{/if} {/if}
</button> </button>
</Tooltip>
<ProjectsPopup bind:this={popup} target={buttonTrigger} {isNavCollapsed} /> <ProjectsPopup bind:this={popup} target={buttonTrigger} {isNavCollapsed} />
</div> </div>

View File

@ -6,7 +6,7 @@
import { getContext } from '$lib/utils/context'; import { getContext } from '$lib/utils/context';
import Badge from '@gitbutler/ui/Badge.svelte'; import Badge from '@gitbutler/ui/Badge.svelte';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import { tooltip } from '@gitbutler/ui/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -38,12 +38,13 @@
{#if !isNavCollapsed} {#if !isNavCollapsed}
<div class="content"> <div class="content">
<div class="button-head"> <div class="button-head">
<span <Tooltip text="The branch your Workspace branches are based on and merge into.">
use:tooltip={'The branch that your Workspace virtual branches are based on and will be merged into.'} <span class="text-14 text-semibold trunk-label">Target</span>
class="text-14 text-semibold trunk-label">Target</span </Tooltip>
>
{#if ($base?.behind || 0) > 0} {#if ($base?.behind || 0) > 0}
<Badge label={$base?.behind || 0} help="Unmerged upstream commits" /> <Tooltip text="Unmerged upstream commits">
<Badge label={$base?.behind || 0} />
</Tooltip>
{/if} {/if}
<SyncButton /> <SyncButton />
</div> </div>

View File

@ -10,7 +10,7 @@
export let loading = false; export let loading = false;
export let disabled = false; export let disabled = false;
export let wide = false; export let wide = false;
export let help = ''; export let tooltip = '';
function persistedAction(projectId: string): Persisted<MergeMethod> { function persistedAction(projectId: string): Persisted<MergeMethod> {
const key = 'projectMergeMethod'; const key = 'projectMergeMethod';
@ -35,7 +35,7 @@
{loading} {loading}
bind:this={dropDown} bind:this={dropDown}
{wide} {wide}
{help} {tooltip}
{disabled} {disabled}
onclick={() => { onclick={() => {
dispatch('click', { method: $action }); dispatch('click', { method: $action });

View File

@ -20,10 +20,10 @@
type Props = { type Props = {
loading: boolean; loading: boolean;
disabled: boolean; disabled: boolean;
help: string; tooltip: string;
click: (opts: { draft: boolean }) => void; click: (opts: { draft: boolean }) => void;
}; };
const { loading, disabled, help, click }: Props = $props(); const { loading, disabled, tooltip, click }: Props = $props();
const preferredAction = persisted<Action>(Action.Create, 'projectDefaultPrAction'); const preferredAction = persisted<Action>(Action.Create, 'projectDefaultPrAction');
let dropDown: DropDownButton; let dropDown: DropDownButton;
@ -38,7 +38,7 @@
<DropDownButton <DropDownButton
style="ghost" style="ghost"
outline outline
{help} {tooltip}
{disabled} {disabled}
{loading} {loading}
bind:this={dropDown} bind:this={dropDown}

View File

@ -169,7 +169,7 @@
style="ghost" style="ghost"
outline outline
loading={$mrLoading || $checksLoading} loading={$mrLoading || $checksLoading}
help={$timeAgo ? 'Updated ' + $timeAgo : ''} tooltip={$timeAgo ? 'Updated ' + $timeAgo : ''}
onclick={async () => { onclick={async () => {
$checksMonitor?.update(); $checksMonitor?.update();
$prMonitor?.refresh(); $prMonitor?.refresh();
@ -231,7 +231,7 @@
!$pr.mergeable || !$pr.mergeable ||
['dirty', 'unknown', 'blocked', 'behind'].includes($pr.mergeableState)} ['dirty', 'unknown', 'blocked', 'behind'].includes($pr.mergeableState)}
loading={isMerging} loading={isMerging}
help="Merge pull request and refresh" tooltip="Merge pull request and refresh"
on:click={async (e) => { on:click={async (e) => {
if (!$pr) return; if (!$pr) return;
isMerging = true; isMerging = true;

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte'; import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import type iconsJson from '@gitbutler/ui/data/icons.json'; import type iconsJson from '@gitbutler/ui/data/icons.json';
import type { ComponentColor, ComponentStyleKind } from '@gitbutler/ui/utils/colorTypes'; import type { ComponentColor, ComponentStyleKind } from '@gitbutler/ui/utils/colorTypes';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@ -13,7 +14,7 @@
disabled?: boolean; disabled?: boolean;
loading?: boolean; loading?: boolean;
wide?: boolean; wide?: boolean;
help?: string; tooltip?: string;
menuPosition?: 'top' | 'bottom'; menuPosition?: 'top' | 'bottom';
children: Snippet; children: Snippet;
contextMenuSlot: Snippet; contextMenuSlot: Snippet;
@ -28,7 +29,7 @@
disabled = false, disabled = false,
loading = false, loading = false,
wide = false, wide = false,
help = '', tooltip,
menuPosition = 'bottom', menuPosition = 'bottom',
children, children,
contextMenuSlot, contextMenuSlot,
@ -50,17 +51,17 @@
} }
</script> </script>
<div class="dropdown-wrapper" class:wide> <Tooltip text={tooltip}>
<div class="dropdown-wrapper" class:wide>
<div class="dropdown"> <div class="dropdown">
<Button <Button
{style} {style}
{icon} {icon}
{kind} {kind}
{help}
{outline} {outline}
reversedDirection reversedDirection
disabled={disabled || loading} disabled={disabled || loading}
isDropdownChild dropdownChild
{onclick} {onclick}
> >
{@render children()} {@render children()}
@ -69,12 +70,11 @@
bind:el={iconEl} bind:el={iconEl}
{style} {style}
{kind} {kind}
{help}
{outline} {outline}
icon={visible ? 'chevron-up' : 'chevron-down'} icon={visible ? 'chevron-up' : 'chevron-down'}
{loading} {loading}
disabled={disabled || loading} disabled={disabled || loading}
isDropdownChild dropdownChild
onclick={() => { onclick={() => {
visible = !visible; visible = !visible;
contextMenu?.toggle(); contextMenu?.toggle();
@ -91,7 +91,8 @@
> >
{@render contextMenuSlot()} {@render contextMenuSlot()}
</ContextMenu> </ContextMenu>
</div> </div>
</Tooltip>
<style lang="postcss"> <style lang="postcss">
.dropdown-wrapper { .dropdown-wrapper {

View File

@ -1,11 +1,8 @@
<script lang="ts"> <script lang="ts">
import { tooltip } from '@gitbutler/ui/utils/tooltip';
export let small = false; export let small = false;
export let disabled = false; export let disabled = false;
export let checked = false; export let checked = false;
export let value = ''; export let value = '';
export let help = '';
export let id = ''; export let id = '';
</script> </script>
@ -18,7 +15,6 @@
{value} {value}
{id} {id}
{disabled} {disabled}
use:tooltip={help}
/> />
<style lang="postcss"> <style lang="postcss">

View File

@ -1,51 +0,0 @@
import { pxToRem } from '@gitbutler/ui/utils/pxToRem';
import { sineInOut } from 'svelte/easing';
import { slide, type SlideParams, type TransitionConfig } from 'svelte/transition';
export function slideFade(node: Element, options: SlideParams): TransitionConfig {
const slideTrans: TransitionConfig = slide(node, options);
return {
...slideTrans,
css: (t, u) =>
`${slideTrans.css ? slideTrans.css(t, u) : ''}
opacity:${t};`
};
}
// extend SlideParams with opacity
type SlideFadeParams = SlideParams & {
opacity?: number;
minHeight?: number;
animateTopPadding?: boolean;
animateBottomPadding?: boolean;
animateLeftPadding?: boolean;
animateRightPadding?: boolean;
};
export function slideFadeExt(node: Element, options: SlideFadeParams): TransitionConfig {
const slideTrans: TransitionConfig = slide(node, options);
const currentHeight = node.clientHeight;
const minHeight = options.minHeight || 0;
const currentPadding = {
top: pxToRem(+getComputedStyle(node).paddingTop),
bottom: pxToRem(+getComputedStyle(node).paddingBottom),
left: pxToRem(+getComputedStyle(node).paddingLeft),
right: pxToRem(+getComputedStyle(node).paddingRight)
};
return {
...slideTrans,
easing: sineInOut,
css: (t) =>
`height: ${minHeight + (currentHeight - minHeight) * Math.min(Math.max(t, 0), 1)}px;
opacity:${t};
padding-top: ${options.animateTopPadding ? 0 : currentPadding.top};
padding-bottom: ${options.animateBottomPadding ? 0 : currentPadding.bottom};
padding-left: ${options.animateLeftPadding ? 0 : currentPadding.left};
padding-right: ${options.animateRightPadding ? 0 : currentPadding.right};
`
};
}

View File

@ -17,7 +17,7 @@
"package:svelte": "svelte-kit sync && svelte-package", "package:svelte": "svelte-kit sync && svelte-package",
"package:styles": "postcss ./src/styles/main.css -o ./dist/styles/main.css && pnpm run copy-fonts", "package:styles": "postcss ./src/styles/main.css -o ./dist/styles/main.css && pnpm run copy-fonts",
"copy-fonts": "postcss ./src/styles/fonts.css -o ./dist/styles/fonts.css && cpy './src/styles/fonts/**/*.woff2' './dist/styles/fonts' --parents", "copy-fonts": "postcss ./src/styles/fonts.css -o ./dist/styles/fonts.css && cpy './src/styles/fonts/**/*.woff2' './dist/styles/fonts' --parents",
"design-tokens:build": "npx tz build && prettier --write ./src/lib/design-tokens.json ./src/styles/core/design-tokens.css", "design-tokens:build": "npx tz build && prettier --write ./src/lib/data/design-tokens.json ./src/styles/core/design-tokens.css",
"prepublishOnly": "pnpm run package", "prepublishOnly": "pnpm run package",
"prepare": "svelte-kit sync", "prepare": "svelte-kit sync",
"storybook": "storybook dev --no-open -p 6006", "storybook": "storybook dev --no-open -p 6006",

View File

@ -3,18 +3,16 @@
export interface BadgeProps { export interface BadgeProps {
label: string | number; label: string | number;
help?: string;
style?: ComponentColor; style?: ComponentColor;
kind?: ComponentStyleKind; kind?: ComponentStyleKind;
} }
</script> </script>
<script lang="ts"> <script lang="ts">
import { tooltip } from '$lib/utils/tooltip'; const { label, style = 'neutral', kind = 'solid' }: BadgeProps = $props();
let { label, help, style = 'neutral', kind = 'solid' }: BadgeProps = $props();
</script> </script>
<div class="badge {style} {kind} text-10 text-semibold" use:tooltip={help}> <div class="badge {style} {kind} text-10 text-semibold">
{label} {label}
</div> </div>

View File

@ -1,6 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { ComponentColor, ComponentStyleKind } from '$lib/utils/colorTypes';
export interface ButtonProps { export interface ButtonProps {
el?: HTMLElement; el?: HTMLElement;
// Interaction props // Interaction props
@ -18,7 +16,7 @@
wide?: boolean; wide?: boolean;
grow?: boolean; grow?: boolean;
align?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline' | 'auto'; align?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline' | 'auto';
isDropdownChild?: boolean; dropdownChild?: boolean;
// Style props // Style props
style?: ComponentColor; style?: ComponentColor;
kind?: ComponentStyleKind; kind?: ComponentStyleKind;
@ -27,7 +25,9 @@
solidBackground?: boolean; solidBackground?: boolean;
// Additional elements // Additional elements
icon?: keyof typeof iconsJson | undefined; icon?: keyof typeof iconsJson | undefined;
help?: string; tooltip?: string;
tooltipPosition?: TooltipPosition;
tooltipAlign?: TooltipAlign;
helpShowDelay?: number; helpShowDelay?: number;
testId?: string; testId?: string;
// Events // Events
@ -41,10 +41,11 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import Tooltip, { type TooltipAlign, type TooltipPosition } from './Tooltip.svelte';
import Icon from '$lib/Icon.svelte'; import Icon from '$lib/Icon.svelte';
import { pxToRem } from '$lib/utils/pxToRem'; import { pxToRem } from '$lib/utils/pxToRem';
import { tooltip } from '$lib/utils/tooltip';
import type iconsJson from '$lib/data/icons.json'; import type iconsJson from '$lib/data/icons.json';
import type { ComponentColor, ComponentStyleKind } from '$lib/utils/colorTypes';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
let { let {
@ -62,7 +63,7 @@
wide = false, wide = false,
grow = false, grow = false,
align = 'auto', align = 'auto',
isDropdownChild = false, dropdownChild = false,
style = 'neutral', style = 'neutral',
kind = 'soft', kind = 'soft',
outline = false, outline = false,
@ -70,8 +71,9 @@
solidBackground = false, solidBackground = false,
testId, testId,
icon, icon,
help = '', tooltip,
helpShowDelay = 1200, tooltipPosition,
tooltipAlign,
onclick, onclick,
onmousedown, onmousedown,
oncontextmenu, oncontextmenu,
@ -89,7 +91,8 @@
} }
</script> </script>
<button <Tooltip text={tooltip} align={tooltipAlign} position={tooltipPosition}>
<button
bind:this={el} bind:this={el}
class="btn focus-state {style} {kind} {size}-size" class="btn focus-state {style} {kind} {size}-size"
class:outline class:outline
@ -99,15 +102,11 @@
class:shrinkable class:shrinkable
class:wide class:wide
class:grow class:grow
class:is-dropdown={dropdownChild}
class:not-clickable={!clickable} class:not-clickable={!clickable}
class:fixed-width={!children && !wide} class:fixed-width={!children && !wide}
class:is-dropdown={isDropdownChild}
style:align-self={align} style:align-self={align}
style:width={width ? pxToRem(width) : undefined} style:width={width ? pxToRem(width) : undefined}
use:tooltip={{
text: help,
delay: helpShowDelay
}}
disabled={disabled || loading} disabled={disabled || loading}
onclick={handleAction} onclick={handleAction}
{onmousedown} {onmousedown}
@ -117,7 +116,7 @@
{id} {id}
{...testId ? { 'data-testid': testId } : null} {...testId ? { 'data-testid': testId } : null}
tabindex={clickable ? tabindex : -1} tabindex={clickable ? tabindex : -1}
> >
{#if children} {#if children}
<span <span
class="label text-semibold" class="label text-semibold"
@ -137,7 +136,8 @@
{/if} {/if}
</div> </div>
{/if} {/if}
</button> </button>
</Tooltip>
<style lang="postcss"> <style lang="postcss">
.btn { .btn {

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/Icon.svelte'; import Icon from '$lib/Icon.svelte';
import TimeAgo from '$lib/TimeAgo.svelte'; import TimeAgo from '$lib/TimeAgo.svelte';
import { tooltip } from '$lib/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
interface Props { interface Props {
@ -55,8 +55,6 @@
observer.disconnect(); observer.disconnect();
}; };
}); });
const tooltipDelay = 500;
</script> </script>
<button class="branch" class:selected onmousedown={onMouseDown} bind:this={intersectionTarget}> <button class="branch" class:selected onmousedown={onMouseDown} bind:this={intersectionTarget}>
@ -84,8 +82,8 @@
<div class="row-group"> <div class="row-group">
{#if pullRequestDetails} {#if pullRequestDetails}
<Tooltip text={pullRequestDetails.title}>
<div <div
use:tooltip={{ text: pullRequestDetails.title, delay: tooltipDelay }}
class="branch-tag tag-pr" class="branch-tag tag-pr"
class:tag-pr={!pullRequestDetails.draft} class:tag-pr={!pullRequestDetails.draft}
class:tag-draft-pr={pullRequestDetails.draft} class:tag-draft-pr={pullRequestDetails.draft}
@ -95,6 +93,7 @@
</span> </span>
<Icon name="pr-small" /> <Icon name="pr-small" />
</div> </div>
</Tooltip>
{/if} {/if}
{#if applied} {#if applied}
<div class="branch-tag tag-applied"> <div class="branch-tag tag-applied">
@ -106,15 +105,14 @@
<div class="row"> <div class="row">
{#if lastCommitDetails?.lastCommitAt} {#if lastCommitDetails?.lastCommitAt}
<span <Tooltip text={lastCommitDetails.lastCommitAt.toLocaleString('en-GB')}>
class="branch-time text-11 details truncate" <span class="branch-time text-11 details truncate">
use:tooltip={lastCommitDetails.lastCommitAt.toLocaleString('en-GB')}
>
{#if lastCommitDetails} {#if lastCommitDetails}
<TimeAgo date={lastCommitDetails.lastCommitAt} addSuffix /> <TimeAgo date={lastCommitDetails.lastCommitAt} addSuffix />
by {lastCommitDetails.authorName} by {lastCommitDetails.authorName}
{/if} {/if}
</span> </span>
</Tooltip>
{:else} {:else}
<span class="branch-time text-11 details truncate"> <span class="branch-time text-11 details truncate">
{#if lastCommitDetails} {#if lastCommitDetails}
@ -125,24 +123,15 @@
<div class="stats"> <div class="stats">
{#if branchDetails} {#if branchDetails}
<div <Tooltip text="Code changes">
use:tooltip={{ <div class="code-changes">
text: 'Code changes',
delay: tooltipDelay
}}
class="code-changes"
>
<span class="text-10 text-semibold">+{branchDetails.linesAdded}</span> <span class="text-10 text-semibold">+{branchDetails.linesAdded}</span>
<span class="text-10 text-semibold">-{branchDetails.linesRemoved}</span> <span class="text-10 text-semibold">-{branchDetails.linesRemoved}</span>
</div> </div>
</Tooltip>
<div <Tooltip text="Number of commits">
use:tooltip={{ <div class="branch-tag tag-commits">
text: 'Number of commits',
delay: tooltipDelay
}}
class="branch-tag tag-commits"
>
<svg <svg
width="12" width="12"
height="8" height="8"
@ -157,6 +146,7 @@
<span class="text-10 text-semibold">{branchDetails.commitCount}</span> <span class="text-10 text-semibold">{branchDetails.commitCount}</span>
</div> </div>
</Tooltip>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -0,0 +1,142 @@
<script lang="ts" context="module">
export type TooltipPosition = 'top' | 'bottom';
export type TooltipAlign = 'start' | 'center' | 'end';
</script>
<script lang="ts">
import { portal } from '$lib/utils/portal';
import { flyScale } from '$lib/utils/transitions';
import { type Snippet } from 'svelte';
interface Props {
text?: string;
delay?: number;
align?: TooltipAlign;
position?: TooltipPosition;
children: Snippet;
}
const { text, delay = 700, align = 'center', position = 'bottom', children }: Props = $props();
let targetEl: HTMLElement | undefined = $state();
let tooltipEl: HTMLElement | undefined = $state();
let show = $state(false);
let timeoutId: undefined | ReturnType<typeof setTimeout> = $state();
const isTextEmpty = $derived(!text || text === '');
function handleMouseEnter() {
timeoutId = setTimeout(() => {
show = true;
// console.log('showing tooltip');
}, delay); // 500ms delay before showing the tooltip
}
function handleMouseLeave() {
clearTimeout(timeoutId);
show = false;
}
function adjustPosition() {
if (!targetEl || !tooltipEl) return;
const tooltipRect = tooltipEl.getBoundingClientRect();
// get first child of targetEl
const targetChild = targetEl.children[0];
const targetRect = targetChild.getBoundingClientRect();
let top = 0;
let left = 0;
let transformOriginTop = 'center';
let transformOriginLeft = 'center';
const gap = 4;
if (position === 'bottom') {
top = targetRect.bottom + window.scrollY + gap;
transformOriginTop = 'top';
} else if (position === 'top') {
top = targetRect.top - tooltipRect.height + window.scrollY - gap;
transformOriginTop = 'bottom';
}
if (align === 'start') {
left = targetRect.left + window.scrollX;
transformOriginLeft = 'left';
} else if (align === 'end') {
left = targetRect.right - tooltipRect.width + window.scrollX;
transformOriginLeft = 'right';
} else if (align === 'center') {
left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 + window.scrollX;
transformOriginLeft = 'center';
}
tooltipEl.style.top = `${top}px`;
tooltipEl.style.left = `${left}px`;
tooltipEl.style.transformOrigin = `${transformOriginTop} ${transformOriginLeft}`;
}
$effect(() => {
if (tooltipEl) {
adjustPosition();
}
});
</script>
{#if isTextEmpty}
{@render children()}
{:else}
<span
bind:this={targetEl}
class="tooltip-wrap"
role="tooltip"
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
>
{#if children}
{@render children()}
{/if}
{#if show}
<div
bind:this={tooltipEl}
use:portal={'body'}
class="tooltip-container text-11 text-body"
transition:flyScale={{
position: position
}}
>
<span>{text}</span>
</div>
{/if}
</span>
{/if}
<style lang="postcss">
.tooltip-wrap {
position: relative;
display: contents;
}
.tooltip-container {
white-space: pre-line;
display: flex;
justify-content: center;
flex-direction: column;
position: fixed;
pointer-events: none;
background-color: var(--clr-tooltip-bg);
border: 1px solid var(--clr-tooltip-border);
border-radius: var(--radius-m);
color: var(--clr-core-ntrl-80);
display: inline-block;
width: fit-content;
max-width: 240px;
padding: 4px 8px;
z-index: var(--z-blocker);
text-align: left;
box-shadow: var(--fx-shadow-s);
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Tooltip from '$lib/Tooltip.svelte';
import { stringToColor } from '$lib/utils/stringToColor'; import { stringToColor } from '$lib/utils/stringToColor';
import { tooltip as tooltipComponent } from '$lib/utils/tooltip';
interface Props { interface Props {
srcUrl: string; srcUrl: string;
@ -13,7 +13,8 @@
const { srcUrl, tooltip, size = 'small' }: Props = $props(); const { srcUrl, tooltip, size = 'small' }: Props = $props();
</script> </script>
<div class="image-wrapper {size}" style:background-color={stringToColor(tooltip)}> <Tooltip text={tooltip}>
<div class="image-wrapper {size}" style:background-color={stringToColor(tooltip)}>
<img <img
class="avatar" class="avatar"
alt={tooltip} alt={tooltip}
@ -21,12 +22,9 @@
loading="lazy" loading="lazy"
onload={() => (isLoaded = true)} onload={() => (isLoaded = true)}
class:show={isLoaded} class:show={isLoaded}
use:tooltipComponent={{
text: tooltip,
delay: 500
}}
/> />
</div> </div>
</Tooltip>
<style lang="postcss"> <style lang="postcss">
.image-wrapper { .image-wrapper {

View File

@ -10,7 +10,7 @@
<script lang="ts"> <script lang="ts">
import Avatar from './Avatar.svelte'; import Avatar from './Avatar.svelte';
import { tooltip } from '$lib/utils/tooltip'; import Tooltip from '$lib/Tooltip.svelte';
const { avatars, maxAvatars = 5 }: Props = $props(); const { avatars, maxAvatars = 5 }: Props = $props();
@ -43,15 +43,11 @@
{/if} {/if}
{/each} {/each}
{#if avatars.length > maxAvatars} {#if avatars.length > maxAvatars}
<div <Tooltip text={getTooltipText() || 'mr. unknown'}>
class="avatars-counter" <div class="avatars-counter">
use:tooltip={{
text: getTooltipText() || 'mr. unknown',
delay: 500
}}
>
<span class="text-11 text-semibold">+{avatars.length - maxAvatars}</span> <span class="text-11 text-semibold">+{avatars.length - maxAvatars}</span>
</div> </div>
</Tooltip>
{/if} {/if}
</div> </div>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Tooltip from '$lib/Tooltip.svelte';
import Avatar from '$lib/avatar/Avatar.svelte'; import Avatar from '$lib/avatar/Avatar.svelte';
import { tooltip } from '$lib/utils/tooltip';
import { isDefined } from '$lib/utils/typeguards'; import { isDefined } from '$lib/utils/typeguards';
import type { CommitNodeData, Color } from '$lib/commitLines/types'; import type { CommitNodeData, Color } from '$lib/commitLines/types';
@ -37,7 +37,9 @@
<Avatar srcUrl={commitNode.commit?.author.gravatarUrl ?? ''} tooltip={hoverText} /> <Avatar srcUrl={commitNode.commit?.author.gravatarUrl ?? ''} tooltip={hoverText} />
</div> </div>
{:else} {:else}
<div class="small-node" use:tooltip={hoverText}></div> <Tooltip text={hoverText}>
<div class="small-node"></div>
</Tooltip>
{/if} {/if}
</div> </div>
{/if} {/if}

View File

@ -795,7 +795,7 @@
}, },
"30": { "30": {
"$type": "color", "$type": "color",
"$value": "#2a7e57", "$value": "#287b55",
"$description": "", "$description": "",
"$extensions": { "$extensions": {
"mode": {}, "mode": {},
@ -811,7 +811,7 @@
}, },
"40": { "40": {
"$type": "color", "$type": "color",
"$value": "#469b73", "$value": "#3c9a6f",
"$description": "", "$description": "",
"$extensions": { "$extensions": {
"mode": {}, "mode": {},
@ -827,7 +827,7 @@
}, },
"50": { "50": {
"$type": "color", "$type": "color",
"$value": "#4eb182", "$value": "#4ab582",
"$description": "", "$description": "",
"$extensions": { "$extensions": {
"mode": {}, "mode": {},
@ -843,7 +843,7 @@
}, },
"60": { "60": {
"$type": "color", "$type": "color",
"$value": "#a1ceb9", "$value": "#92ddba",
"$description": "", "$description": "",
"$extensions": { "$extensions": {
"mode": {}, "mode": {},
@ -859,7 +859,7 @@
}, },
"70": { "70": {
"$type": "color", "$type": "color",
"$value": "#cae8da", "$value": "#bef4da",
"$description": "", "$description": "",
"$extensions": { "$extensions": {
"mode": {}, "mode": {},
@ -875,7 +875,7 @@
}, },
"80": { "80": {
"$type": "color", "$type": "color",
"$value": "#d6f0e4", "$value": "#d0f7e5",
"$description": "", "$description": "",
"$extensions": { "$extensions": {
"mode": {}, "mode": {},
@ -891,7 +891,7 @@
}, },
"90": { "90": {
"$type": "color", "$type": "color",
"$value": "#e8f7f0", "$value": "#e5faf0",
"$description": "", "$description": "",
"$extensions": { "$extensions": {
"mode": {}, "mode": {},
@ -3921,6 +3921,46 @@
} }
} }
} }
},
"tooltip": {
"border": {
"$type": "color",
"$value": "{clr-core.ntrl.10}",
"$description": "",
"$extensions": {
"mode": {
"light": "{clr-core.ntrl.10}",
"dark": "{clr-core.ntrl.30}"
},
"figma": {
"variableId": "VariableID:4330:3683",
"collection": {
"id": "VariableCollectionId:8:1868",
"name": "clr",
"defaultModeId": "8:5"
}
}
}
},
"bg": {
"$type": "color",
"$value": "{clr-core.ntrl.10}",
"$description": "",
"$extensions": {
"mode": {
"light": "{clr-core.ntrl.10}",
"dark": "{clr-core.ntrl.10}"
},
"figma": {
"variableId": "VariableID:4330:3697",
"collection": {
"id": "VariableCollectionId:8:1868",
"name": "clr",
"defaultModeId": "8:5"
}
}
}
}
} }
}, },
"size": { "size": {
@ -4097,7 +4137,7 @@
"useDTCGKeys": true, "useDTCGKeys": true,
"colorMode": "hex", "colorMode": "hex",
"variableCollections": ["clr-core", "clr", "size", "radius"], "variableCollections": ["clr-core", "clr", "size", "radius"],
"createdAt": "2024-08-10T21:58:58.533Z" "createdAt": "2024-08-31T23:16:13.487Z"
} }
} }
} }

View File

@ -3,7 +3,7 @@
import Checkbox from '$lib/Checkbox.svelte'; import Checkbox from '$lib/Checkbox.svelte';
import Icon from '$lib/Icon.svelte'; import Icon from '$lib/Icon.svelte';
import FileIcon from '$lib/file/FileIcon.svelte'; import FileIcon from '$lib/file/FileIcon.svelte';
import { tooltip } from '$lib/utils/tooltip'; import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import type { FileStatus } from './types'; import type { FileStatus } from './types';
interface Props { interface Props {
@ -95,9 +95,11 @@
<div class="details"> <div class="details">
{#if locked} {#if locked}
<div class="locked" use:tooltip={{ text: lockText ?? '', delay: 500 }}> <Tooltip text={lockText}>
<div class="locked">
<Icon name="locked-small" color="warning" /> <Icon name="locked-small" color="warning" />
</div> </div>
</Tooltip>
{/if} {/if}
{#if conflicted} {#if conflicted}

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { tooltip } from '$lib/utils/tooltip';
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from 'svelte';
import type { SegmentContext } from './segmentTypes'; import type { SegmentContext } from './segmentTypes';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@ -8,11 +7,10 @@
id: string; id: string;
onselect?: (id: string) => void; onselect?: (id: string) => void;
disabled?: boolean; disabled?: boolean;
tooltipText?: string;
children: Snippet; children: Snippet;
} }
const { id, onselect, children, tooltipText, disabled = false }: SegmentProps = $props(); const { id, onselect, children, disabled = false }: SegmentProps = $props();
const context = getContext<SegmentContext>('SegmentControl'); const context = getContext<SegmentContext>('SegmentControl');
const index = context.setIndex(); const index = context.setIndex();
@ -63,10 +61,6 @@
} }
} }
}} }}
use:tooltip={{
text: tooltipText ? tooltipText : '',
delay: 1000
}}
> >
<span class="text-12 label"> <span class="text-12 label">
{@render children()} {@render children()}

View File

@ -1,119 +0,0 @@
export interface ToolTipOptions {
text: string;
noMaxWidth?: boolean;
delay?: number;
}
const defaultOptions: Partial<ToolTipOptions> = {
delay: 1200,
noMaxWidth: false
};
export function tooltip(node: HTMLElement, optsOrString: ToolTipOptions | string | undefined) {
// The tooltip element we are adding to the dom
let tooltip: HTMLDivElement | undefined;
// Note that we use this both for delaying show, as well as delaying hide
let timeoutId: any;
// Options
let { text, delay, noMaxWidth } = defaultOptions;
// Most use cases only involve passing a string, so we allow either opts of
// simple text.
function setOpts(opts: ToolTipOptions | string | undefined) {
if (typeof opts === 'string') {
text = opts;
} else if (opts) {
({ text, delay, noMaxWidth } = opts || {});
}
if (tooltip && text) tooltip.innerText = text;
}
setOpts(optsOrString);
function onPointerEnter() {
// If tooltip is displayed we clear hide timeout
if (tooltip && timeoutId) clearTimeout(timeoutId);
// If no tooltip and no timeout id we set a show timeout
else if (!tooltip && !timeoutId) timeoutId = setTimeout(() => show(), delay);
}
function onPointerLeave() {
// If tooltip shown when mouse out then we hide after delay
if (tooltip) hide();
// But if we mouse out before tooltip is shown, we cancel the show timer
else if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
}
function show() {
if (!text || !node.isConnected) return;
tooltip = document.createElement('div') as HTMLDivElement;
// TODO: Can we co-locate tooltip.js & tooltip.postcss?
tooltip.classList.add('tooltip', 'text-11'); // see tooltip.postcss
if (noMaxWidth) tooltip.classList.add('no-max-width');
tooltip.innerText = text;
document.body.appendChild(tooltip);
adjustPosition();
}
function hide() {
if (tooltip) tooltip.remove();
tooltip = undefined;
timeoutId = undefined;
}
function adjustPosition() {
if (!tooltip) return;
// Dimensions and position of target element
const nodeRect = node.getBoundingClientRect();
const nodeHeight = nodeRect.height;
const nodeWidth = nodeRect.width;
const nodeLeft = nodeRect.left;
const nodeTop = nodeRect.top;
// Padding
const padding = 4;
// Window dimensions
const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;
const tooltipHeight = tooltip.offsetHeight;
const tooltipWidth = tooltip.offsetWidth;
const showBelow = windowHeight > nodeTop + nodeHeight + tooltipHeight + padding;
// Note that we don't check if width of tooltip is wider than the window.
if (showBelow) {
tooltip.style.top = `${nodeTop + nodeHeight + padding}px`;
} else {
tooltip.style.top = `${nodeTop - tooltipHeight - padding}px`;
}
let leftPos = nodeLeft - (tooltipWidth - nodeWidth) / 2;
if (leftPos < padding) leftPos = padding;
if (leftPos + tooltipWidth > windowWidth) leftPos = windowWidth - tooltipWidth - padding;
tooltip.style.left = `${leftPos}px`;
}
node.addEventListener('pointerenter', onPointerEnter);
node.addEventListener('pointerleave', onPointerLeave);
return {
update(opts: ToolTipOptions | string | undefined) {
setOpts(opts);
},
destroy() {
tooltip?.remove();
timeoutId && clearTimeout(timeoutId);
node.removeEventListener('pointerenter', onPointerEnter);
node.removeEventListener('pointerleave', onPointerLeave);
}
};
}

View File

@ -0,0 +1,52 @@
import { pxToRem } from './pxToRem';
import { cubicOut } from 'svelte/easing';
import { slide, type SlideParams, type TransitionConfig } from 'svelte/transition';
export function slideFade(node: Element, options: SlideParams): TransitionConfig {
const slideTrans: TransitionConfig = slide(node, options);
return {
...slideTrans,
css: (t, u) =>
`${slideTrans.css ? slideTrans.css(t, u) : ''}
opacity:${t};`
};
}
export function flyScale(
_: Element,
params: {
y?: number;
x?: number;
start?: number;
duration?: number;
position?: 'top' | 'bottom';
} = {}
): TransitionConfig {
// Default values
const DEFAULT_Y = -6;
const DEFAULT_X = 0;
const DEFAULT_SCALE_START = 0.94;
const DEFAULT_DURATION = 200;
const DEFAULT_POSITION = 'top';
// Extracting and using default values
const y = params.y ?? DEFAULT_Y;
const x = params.x ?? DEFAULT_X;
const startScale = params.start ?? DEFAULT_SCALE_START;
const duration = params.duration ?? DEFAULT_DURATION;
const position = params.position ?? DEFAULT_POSITION;
return {
duration,
css: (t) => {
const translateY = y * (1 - t);
const translateX = x * (1 - t);
const scale = startScale + t * (1 - startScale);
return `transform: translate3d(${pxToRem(translateX)}, ${pxToRem(position === 'top' ? -translateY : translateY)}, 0) scale(${scale});
opacity: ${t};`;
},
easing: cubicOut
};
}

View File

@ -13,7 +13,6 @@ export const BadgeStory: Story = {
name: 'Badge', name: 'Badge',
args: { args: {
label: '127', label: '127',
help: 'This is a badge',
style: 'neutral', style: 'neutral',
kind: 'solid' kind: 'solid'
}, },

View File

@ -23,7 +23,6 @@ export const ButtonDefault: Story = {
outline: false, outline: false,
dashed: false, dashed: false,
solidBackground: false, solidBackground: false,
help: '',
helpShowDelay: 1200, helpShowDelay: 1200,
id: 'button', id: 'button',
tabindex: 0, tabindex: 0,
@ -34,7 +33,7 @@ export const ButtonDefault: Story = {
wide: false, wide: false,
grow: false, grow: false,
align: 'center', align: 'center',
isDropdownChild: false, dropdownChild: false,
onclick: () => { onclick: () => {
console.log('Button clicked'); console.log('Button clicked');
} }
@ -60,9 +59,16 @@ export const ButtonClickable: Story = {
name: 'Not clickable + tooltip', name: 'Not clickable + tooltip',
args: { args: {
clickable: false, clickable: false,
help: 'This button is not clickable', tooltip: 'This button is not clickable',
tooltipAlign: 'start',
onclick: () => { onclick: () => {
console.log('Button clicked'); console.log('Button clicked');
} }
},
argTypes: {
tooltipAlign: {
control: 'select',
options: ['start', 'center', 'end']
}
} }
}; };

View File

@ -21,7 +21,8 @@ const meta = {
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const ModalStory: Story = { export const DefaultStory: Story = {
name: 'Modal',
args: { args: {
width: 'small', width: 'small',
title: 'This is a fantastic modal :D' title: 'This is a fantastic modal :D'

View File

@ -0,0 +1,30 @@
<script lang="ts">
import Tooltip from '$lib/Tooltip.svelte';
const props = $props();
</script>
<div class="wrapper">
<p class="text-13 text">
hello world! Here is a <Tooltip text={props.text}>
<span class="tooltip-text">tooltip</span>
</Tooltip> for you.
</p>
</div>
<style>
.wrapper {
display: flex;
flex-direction: column;
padding: 20px;
}
.text {
color: var(--clr-text-1);
}
.tooltip-text {
text-decoration: underline;
text-decoration-style: dotted;
}
</style>

View File

@ -0,0 +1,30 @@
import DemoTooltip from './DemoTooltip.svelte';
import type { Meta, StoryObj } from '@storybook/svelte';
const meta = {
title: 'Overlays / Tooltip',
component: DemoTooltip
} satisfies Meta<DemoTooltip>;
export default meta;
type Story = StoryObj<typeof meta>;
export const DefaultStory: Story = {
name: 'Tooltip',
args: {
position: 'bottom',
align: 'center',
delay: 500,
text: 'This is a fantastic tooltip :D'
},
argTypes: {
position: {
control: 'select',
options: ['top', 'bottom']
},
align: {
control: 'select',
options: ['start', 'center', 'end']
}
}
};

View File

@ -1,33 +0,0 @@
.tooltip {
pointer-events: none;
background-color: var(--clr-core-ntrl-10);
border-radius: var(--radius-s);
border: 1px solid var(--clr-core-ntrl-30);
color: var(--clr-core-ntrl-60);
display: inline-block;
padding: 6px 8px;
z-index: var(--z-blocker);
max-width: 180px;
position: absolute;
left: -9999px;
top: -9999px;
opacity: 0;
animation: showup-tooltip-animation 0.1s ease-out forwards;
&.no-max-width {
max-width: unset;
}
}
@keyframes showup-tooltip-animation {
from {
opacity: 0;
transform: translateY(-0.2rem) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}

View File

@ -52,13 +52,13 @@
--clr-core-succ-5: color(srgb 0.050980392156862744 0.14901960784313725 0.10196078431372549); --clr-core-succ-5: color(srgb 0.050980392156862744 0.14901960784313725 0.10196078431372549);
--clr-core-succ-10: color(srgb 0.10980392156862745 0.25098039215686274 0.1843137254901961); --clr-core-succ-10: color(srgb 0.10980392156862745 0.25098039215686274 0.1843137254901961);
--clr-core-succ-20: color(srgb 0.13333333333333333 0.3254901960784314 0.23529411764705882); --clr-core-succ-20: color(srgb 0.13333333333333333 0.3254901960784314 0.23529411764705882);
--clr-core-succ-30: color(srgb 0.16470588235294117 0.49411764705882355 0.3411764705882353); --clr-core-succ-30: color(srgb 0.1568627450980392 0.4823529411764706 0.3333333333333333);
--clr-core-succ-40: color(srgb 0.27450980392156865 0.6078431372549019 0.45098039215686275); --clr-core-succ-40: color(srgb 0.23529411764705882 0.6039215686274509 0.43529411764705883);
--clr-core-succ-50: color(srgb 0.3058823529411765 0.6941176470588235 0.5098039215686274); --clr-core-succ-50: color(srgb 0.2901960784313726 0.7098039215686275 0.5098039215686274);
--clr-core-succ-60: color(srgb 0.6313725490196078 0.807843137254902 0.7254901960784313); --clr-core-succ-60: color(srgb 0.5725490196078431 0.8666666666666667 0.7294117647058823);
--clr-core-succ-70: color(srgb 0.792156862745098 0.9098039215686274 0.8549019607843137); --clr-core-succ-70: color(srgb 0.7450980392156863 0.9568627450980393 0.8549019607843137);
--clr-core-succ-80: color(srgb 0.8392156862745098 0.9411764705882353 0.8941176470588236); --clr-core-succ-80: color(srgb 0.8156862745098039 0.9686274509803922 0.8980392156862745);
--clr-core-succ-90: color(srgb 0.9098039215686274 0.9686274509803922 0.9411764705882353); --clr-core-succ-90: color(srgb 0.8980392156862745 0.9803921568627451 0.9411764705882353);
--clr-core-succ-95: color(srgb 0.9647058823529412 0.9882352941176471 0.984313725490196); --clr-core-succ-95: color(srgb 0.9647058823529412 0.9882352941176471 0.984313725490196);
--clr-core-purp-5: color(srgb 0.1568627450980392 0.11372549019607843 0.26666666666666666); --clr-core-purp-5: color(srgb 0.1568627450980392 0.11372549019607843 0.26666666666666666);
--clr-core-purp-10: color(srgb 0.24705882352941178 0.17254901960784313 0.40784313725490196); --clr-core-purp-10: color(srgb 0.24705882352941178 0.17254901960784313 0.40784313725490196);
@ -235,6 +235,8 @@
srgb 0.5294117647058824 0.6588235294117647 0.6039215686274509 srgb 0.5294117647058824 0.6588235294117647 0.6039215686274509
); );
--clr-diff-addition-line-bg: color(srgb 0.8784313725490196 0.984313725490196 0.9411764705882353); --clr-diff-addition-line-bg: color(srgb 0.8784313725490196 0.984313725490196 0.9411764705882353);
--clr-tooltip-border: var(--clr-core-ntrl-10);
--clr-tooltip-bg: var(--clr-core-ntrl-10);
--size-icon: 1rem; --size-icon: 1rem;
--size-tag: 1.375rem; --size-tag: 1.375rem;
--size-button: 1.75rem; --size-button: 1.75rem;
@ -419,6 +421,8 @@
--clr-diff-addition-line-bg: color( --clr-diff-addition-line-bg: color(
srgb 0.054901960784313725 0.1843137254901961 0.1450980392156863 srgb 0.054901960784313725 0.1843137254901961 0.1450980392156863
); );
--clr-tooltip-border: var(--clr-core-ntrl-30);
--clr-tooltip-bg: var(--clr-core-ntrl-10);
} }
.bg-clr1 { .bg-clr1 {

View File

@ -14,7 +14,6 @@
/* COMPONENTS */ /* COMPONENTS */
@import './components/diff.css'; @import './components/diff.css';
@import './components/tooltip.css';
@import './components/text-input.css'; @import './components/text-input.css';
@import './components/commit-lines.css'; @import './components/commit-lines.css';
@import './components/draggable.css'; @import './components/draggable.css';

View File

@ -14,7 +14,7 @@ function clearFxPrefix(id) {
} }
export default defineConfig({ export default defineConfig({
tokens: './src/lib/design-tokens.json', tokens: './src/lib/data/design-tokens.json',
outDir: './src/styles/core', outDir: './src/styles/core',
plugins: [ plugins: [
css({ css({