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"
style="success"
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
>
{:else}
@ -56,7 +56,7 @@
size="tag"
icon="virtual-branch-small"
style="neutral"
help="These changes are in your working directory."
tooltip="Changes are in your working directory"
reversedDirection>Virtual</Button
>
{/if}
@ -68,7 +68,7 @@
style="neutral"
shrinkable
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}
</Button>
@ -81,7 +81,7 @@
style="neutral"
kind="solid"
icon="remote-branch-small"
help="At least some of your changes have been pushed"
tooltip="Some changes have been pushed"
reversedDirection>Remote</Button
>
<Button

View File

@ -132,7 +132,7 @@
<div class="draggable" data-drag-handle>
<Icon name="draggable" />
</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 class="collapsed-lane__info-wrap" bind:clientHeight={headerInfoHeight}>
@ -147,7 +147,7 @@
clickable={false}
style="warning"
kind="soft"
help="Uncommitted changes"
tooltip="Uncommitted changes"
>
{uncommittedChanges}
{uncommittedChanges === 1 ? 'change' : 'changes'}
@ -198,7 +198,7 @@
clickable={false}
icon="locked-small"
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
</Button>
@ -214,7 +214,7 @@
<Button
style="pop"
kind="soft"
help="New changes will land here"
tooltip="New changes will land here"
icon="target"
clickable={false}
>
@ -224,7 +224,7 @@
<Button
style="ghost"
outline
help="When selected, new changes will land here"
tooltip="When selected, new changes land here"
icon="target"
onclick={async () => {
isTargetBranchAnimated = true;
@ -242,7 +242,7 @@
<PullRequestButton
click={async ({ draft }) => await createPr({ draft })}
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}
/>
{/if}

View File

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

View File

@ -10,7 +10,7 @@
import Button from '@gitbutler/ui/Button.svelte';
import Icon from '@gitbutler/ui/Icon.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 { Branch } from '$lib/vbranches/types';
import { goto } from '$app/navigation';
@ -40,13 +40,18 @@
<BranchLabel disabled name={branch.name} />
<div class="header__remote-branch">
{#if remoteBranch}
<div
class="status-tag text-11 text-semibold remote"
use:tooltip={'At least some of your changes have been pushed'}
>
<Icon name="remote-branch-small" />
{localBranch ? 'local and remote' : 'remote'}
</div>
<Tooltip text="At least some of your changes have been pushed'">
<Button
size="tag"
icon="remote-branch-small"
style="neutral"
kind="solid"
clickable={false}
>
{localBranch ? 'local and remote' : 'remote'}
</Button>
</Tooltip>
{#if gitHostBranch}
<Button
size="tag"
@ -93,7 +98,7 @@
<Button
style="ghost"
outline
help="Restores these changes into your working directory"
tooltip="Restores these changes into your working directory"
icon="plus-small"
loading={isApplying}
disabled={$mode?.type !== 'OpenWorkspace'}
@ -120,7 +125,7 @@
<Button
style="ghost"
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"
loading={isDeleting}
disabled={!localBranch}

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@
import { VirtualBranch, LocalFile } from '$lib/vbranches/types';
import Checkbox from '@gitbutler/ui/Checkbox.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 { fly } from 'svelte/transition';
@ -179,57 +179,52 @@
{/if}
{#if title.length > 50}
<div
transition:fly={{ y: 2, duration: 150 }}
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" />
</div>
<Tooltip text={'50 characters or less is best.\nUse description for more details'}>
<div transition:fly={{ y: 2, duration: 150 }} class="commit-box__textarea-tooltip">
<Icon name="idea" />
</div>
</Tooltip>
{/if}
<div
class="commit-box__texarea-actions"
class:commit-box-actions_expanded={isExpanded}
use:tooltip={!aiConfigurationValid
? 'You must be logged in or have provided your own API key to use this feature'
<Tooltip
text={!aiConfigurationValid
? 'You must be logged in or have provided your own API key'
: !$aiGenEnabled
? 'You must have summary generation enabled to use this feature'
: ''}
? 'You must have summary generation enabled'
: undefined}
>
<DropDownButton
style="ghost"
outline
icon="ai-small"
disabled={!($aiGenEnabled && aiConfigurationValid)}
loading={aiLoading}
menuPosition="top"
onclick={async () => await generateCommitMessage($branch.files)}
>
Generate message
<div class="commit-box__texarea-actions" class:commit-box-actions_expanded={isExpanded}>
<DropDownButton
style="ghost"
outline
icon="ai-small"
disabled={!($aiGenEnabled && aiConfigurationValid)}
loading={aiLoading}
menuPosition="top"
onclick={async () => await generateCommitMessage($branch.files)}
>
Generate message
{#snippet contextMenuSlot()}
<ContextMenuSection>
<ContextMenuItem
label="Extra concise"
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
>
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
</ContextMenuItem>
{#snippet contextMenuSlot()}
<ContextMenuSection>
<ContextMenuItem
label="Extra concise"
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
>
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
</ContextMenuItem>
<ContextMenuItem
label="Use emojis 😎"
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
>
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
</ContextMenuItem>
</ContextMenuSection>
{/snippet}
</DropDownButton>
</div>
<ContextMenuItem
label="Use emojis 😎"
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
>
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
</ContextMenuItem>
</ContextMenuSection>
{/snippet}
</DropDownButton>
</div>
</Tooltip>
</div>
{/if}

View File

@ -10,7 +10,7 @@
import Button from '@gitbutler/ui/Button.svelte';
import Checkbox from '@gitbutler/ui/Checkbox.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';
export let base: BaseBranch;
@ -49,7 +49,7 @@
<Button
style="pop"
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'}
onclick={() => {
if ($mergeUpstreamWarningDismissed) {
@ -77,12 +77,9 @@
{/if}
<div>
<h1
class="text-13 info-text text-bold"
use:tooltip={'This is the current base for your virtual branches.'}
>
Local
</h1>
<Tooltip text="Current base for virtual branches.">
<h1 class="text-13 info-text text-bold">Local</h1>
</Tooltip>
{#each base.recentCommits as commit, index}
<CommitCard
{commit}

View File

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

View File

@ -138,7 +138,7 @@
<Button
style="pop"
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
>
</div>

View File

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

View File

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

View File

@ -35,7 +35,7 @@
clickable={false}
icon="locked-small"
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
>
{/if}

View File

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

View File

@ -172,7 +172,7 @@
size="tag"
style="ghost"
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={() => {
dispatch('restoreClick');
}}

View File

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

View File

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

View File

@ -4,7 +4,7 @@
import ProjectAvatar from '$lib/navigation/ProjectAvatar.svelte';
import { getContext } from '$lib/utils/context';
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;
@ -15,23 +15,24 @@
</script>
<div class="wrapper">
<button
bind:this={buttonTrigger}
class="text-input button"
use:tooltip={isNavCollapsed ? project?.title : ''}
on:mousedown={(e) => {
e.preventDefault();
popup.toggle();
}}
>
<ProjectAvatar name={project?.title} />
{#if !isNavCollapsed}
<span class="button__label text-14 text-bold">{project?.title}</span>
<div class="button__icon">
<Icon name="select-chevron" />
</div>
{/if}
</button>
<Tooltip text={isNavCollapsed ? project?.title : ''} align="start">
<button
bind:this={buttonTrigger}
class="text-input button"
on:mousedown={(e) => {
e.preventDefault();
popup.toggle();
}}
>
<ProjectAvatar name={project?.title} />
{#if !isNavCollapsed}
<span class="button__label text-14 text-bold">{project?.title}</span>
<div class="button__icon">
<Icon name="select-chevron" />
</div>
{/if}
</button>
</Tooltip>
<ProjectsPopup bind:this={popup} target={buttonTrigger} {isNavCollapsed} />
</div>

View File

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

View File

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

View File

@ -20,10 +20,10 @@
type Props = {
loading: boolean;
disabled: boolean;
help: string;
tooltip: string;
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');
let dropDown: DropDownButton;
@ -38,7 +38,7 @@
<DropDownButton
style="ghost"
outline
{help}
{tooltip}
{disabled}
{loading}
bind:this={dropDown}

View File

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

View File

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

View File

@ -1,11 +1,8 @@
<script lang="ts">
import { tooltip } from '@gitbutler/ui/utils/tooltip';
export let small = false;
export let disabled = false;
export let checked = false;
export let value = '';
export let help = '';
export let id = '';
</script>
@ -18,7 +15,6 @@
{value}
{id}
{disabled}
use:tooltip={help}
/>
<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: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",
"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",
"prepare": "svelte-kit sync",
"storybook": "storybook dev --no-open -p 6006",

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<script lang="ts">
import Icon from '$lib/Icon.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';
interface Props {
@ -55,8 +55,6 @@
observer.disconnect();
};
});
const tooltipDelay = 500;
</script>
<button class="branch" class:selected onmousedown={onMouseDown} bind:this={intersectionTarget}>
@ -84,17 +82,18 @@
<div class="row-group">
{#if pullRequestDetails}
<div
use:tooltip={{ text: pullRequestDetails.title, delay: tooltipDelay }}
class="branch-tag tag-pr"
class:tag-pr={!pullRequestDetails.draft}
class:tag-draft-pr={pullRequestDetails.draft}
>
<span class="text-10 text-semibold">
{#if !pullRequestDetails.draft}PR{:else}Draft{/if}
</span>
<Icon name="pr-small" />
</div>
<Tooltip text={pullRequestDetails.title}>
<div
class="branch-tag tag-pr"
class:tag-pr={!pullRequestDetails.draft}
class:tag-draft-pr={pullRequestDetails.draft}
>
<span class="text-10 text-semibold">
{#if !pullRequestDetails.draft}PR{:else}Draft{/if}
</span>
<Icon name="pr-small" />
</div>
</Tooltip>
{/if}
{#if applied}
<div class="branch-tag tag-applied">
@ -106,15 +105,14 @@
<div class="row">
{#if lastCommitDetails?.lastCommitAt}
<span
class="branch-time text-11 details truncate"
use:tooltip={lastCommitDetails.lastCommitAt.toLocaleString('en-GB')}
>
{#if lastCommitDetails}
<TimeAgo date={lastCommitDetails.lastCommitAt} addSuffix />
by {lastCommitDetails.authorName}
{/if}
</span>
<Tooltip text={lastCommitDetails.lastCommitAt.toLocaleString('en-GB')}>
<span class="branch-time text-11 details truncate">
{#if lastCommitDetails}
<TimeAgo date={lastCommitDetails.lastCommitAt} addSuffix />
by {lastCommitDetails.authorName}
{/if}
</span>
</Tooltip>
{:else}
<span class="branch-time text-11 details truncate">
{#if lastCommitDetails}
@ -125,38 +123,30 @@
<div class="stats">
{#if branchDetails}
<div
use:tooltip={{
text: 'Code changes',
delay: tooltipDelay
}}
class="code-changes"
>
<span class="text-10 text-semibold">+{branchDetails.linesAdded}</span>
<span class="text-10 text-semibold">-{branchDetails.linesRemoved}</span>
</div>
<Tooltip text="Code changes">
<div class="code-changes">
<span class="text-10 text-semibold">+{branchDetails.linesAdded}</span>
<span class="text-10 text-semibold">-{branchDetails.linesRemoved}</span>
</div>
</Tooltip>
<div
use:tooltip={{
text: 'Number of commits',
delay: tooltipDelay
}}
class="branch-tag tag-commits"
>
<svg
width="12"
height="8"
viewBox="0 0 12 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="6.16675" cy="4" r="2.5" stroke="currentColor" stroke-width="1.5" />
<path d="M8.66675 4H12.0001" stroke="currentColor" stroke-width="1.5" />
<path d="M0.333374 4H3.66671" stroke="currentColor" stroke-width="1.5" />
</svg>
<Tooltip text="Number of commits">
<div class="branch-tag tag-commits">
<svg
width="12"
height="8"
viewBox="0 0 12 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="6.16675" cy="4" r="2.5" stroke="currentColor" stroke-width="1.5" />
<path d="M8.66675 4H12.0001" stroke="currentColor" stroke-width="1.5" />
<path d="M0.333374 4H3.66671" stroke="currentColor" stroke-width="1.5" />
</svg>
<span class="text-10 text-semibold">{branchDetails.commitCount}</span>
</div>
<span class="text-10 text-semibold">{branchDetails.commitCount}</span>
</div>
</Tooltip>
{/if}
</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">
import Tooltip from '$lib/Tooltip.svelte';
import { stringToColor } from '$lib/utils/stringToColor';
import { tooltip as tooltipComponent } from '$lib/utils/tooltip';
interface Props {
srcUrl: string;
@ -13,20 +13,18 @@
const { srcUrl, tooltip, size = 'small' }: Props = $props();
</script>
<div class="image-wrapper {size}" style:background-color={stringToColor(tooltip)}>
<img
class="avatar"
alt={tooltip}
src={srcUrl}
loading="lazy"
onload={() => (isLoaded = true)}
class:show={isLoaded}
use:tooltipComponent={{
text: tooltip,
delay: 500
}}
/>
</div>
<Tooltip text={tooltip}>
<div class="image-wrapper {size}" style:background-color={stringToColor(tooltip)}>
<img
class="avatar"
alt={tooltip}
src={srcUrl}
loading="lazy"
onload={() => (isLoaded = true)}
class:show={isLoaded}
/>
</div>
</Tooltip>
<style lang="postcss">
.image-wrapper {

View File

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

View File

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

View File

@ -795,7 +795,7 @@
},
"30": {
"$type": "color",
"$value": "#2a7e57",
"$value": "#287b55",
"$description": "",
"$extensions": {
"mode": {},
@ -811,7 +811,7 @@
},
"40": {
"$type": "color",
"$value": "#469b73",
"$value": "#3c9a6f",
"$description": "",
"$extensions": {
"mode": {},
@ -827,7 +827,7 @@
},
"50": {
"$type": "color",
"$value": "#4eb182",
"$value": "#4ab582",
"$description": "",
"$extensions": {
"mode": {},
@ -843,7 +843,7 @@
},
"60": {
"$type": "color",
"$value": "#a1ceb9",
"$value": "#92ddba",
"$description": "",
"$extensions": {
"mode": {},
@ -859,7 +859,7 @@
},
"70": {
"$type": "color",
"$value": "#cae8da",
"$value": "#bef4da",
"$description": "",
"$extensions": {
"mode": {},
@ -875,7 +875,7 @@
},
"80": {
"$type": "color",
"$value": "#d6f0e4",
"$value": "#d0f7e5",
"$description": "",
"$extensions": {
"mode": {},
@ -891,7 +891,7 @@
},
"90": {
"$type": "color",
"$value": "#e8f7f0",
"$value": "#e5faf0",
"$description": "",
"$extensions": {
"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": {
@ -4097,7 +4137,7 @@
"useDTCGKeys": true,
"colorMode": "hex",
"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 Icon from '$lib/Icon.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';
interface Props {
@ -95,9 +95,11 @@
<div class="details">
{#if locked}
<div class="locked" use:tooltip={{ text: lockText ?? '', delay: 500 }}>
<Icon name="locked-small" color="warning" />
</div>
<Tooltip text={lockText}>
<div class="locked">
<Icon name="locked-small" color="warning" />
</div>
</Tooltip>
{/if}
{#if conflicted}

View File

@ -1,5 +1,4 @@
<script lang="ts">
import { tooltip } from '$lib/utils/tooltip';
import { getContext, onMount } from 'svelte';
import type { SegmentContext } from './segmentTypes';
import type { Snippet } from 'svelte';
@ -8,11 +7,10 @@
id: string;
onselect?: (id: string) => void;
disabled?: boolean;
tooltipText?: string;
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 index = context.setIndex();
@ -63,10 +61,6 @@
}
}
}}
use:tooltip={{
text: tooltipText ? tooltipText : '',
delay: 1000
}}
>
<span class="text-12 label">
{@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',
args: {
label: '127',
help: 'This is a badge',
style: 'neutral',
kind: 'solid'
},

View File

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

View File

@ -21,7 +21,8 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const ModalStory: Story = {
export const DefaultStory: Story = {
name: 'Modal',
args: {
width: 'small',
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-10: color(srgb 0.10980392156862745 0.25098039215686274 0.1843137254901961);
--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-40: color(srgb 0.27450980392156865 0.6078431372549019 0.45098039215686275);
--clr-core-succ-50: color(srgb 0.3058823529411765 0.6941176470588235 0.5098039215686274);
--clr-core-succ-60: color(srgb 0.6313725490196078 0.807843137254902 0.7254901960784313);
--clr-core-succ-70: color(srgb 0.792156862745098 0.9098039215686274 0.8549019607843137);
--clr-core-succ-80: color(srgb 0.8392156862745098 0.9411764705882353 0.8941176470588236);
--clr-core-succ-90: color(srgb 0.9098039215686274 0.9686274509803922 0.9411764705882353);
--clr-core-succ-30: color(srgb 0.1568627450980392 0.4823529411764706 0.3333333333333333);
--clr-core-succ-40: color(srgb 0.23529411764705882 0.6039215686274509 0.43529411764705883);
--clr-core-succ-50: color(srgb 0.2901960784313726 0.7098039215686275 0.5098039215686274);
--clr-core-succ-60: color(srgb 0.5725490196078431 0.8666666666666667 0.7294117647058823);
--clr-core-succ-70: color(srgb 0.7450980392156863 0.9568627450980393 0.8549019607843137);
--clr-core-succ-80: color(srgb 0.8156862745098039 0.9686274509803922 0.8980392156862745);
--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-purp-5: color(srgb 0.1568627450980392 0.11372549019607843 0.26666666666666666);
--clr-core-purp-10: color(srgb 0.24705882352941178 0.17254901960784313 0.40784313725490196);
@ -235,6 +235,8 @@
srgb 0.5294117647058824 0.6588235294117647 0.6039215686274509
);
--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-tag: 1.375rem;
--size-button: 1.75rem;
@ -419,6 +421,8 @@
--clr-diff-addition-line-bg: color(
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 {

View File

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

View File

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