UI: Lane and commit messages design (#3868)

* UI: Remove icons for commit groups

* UI: added paddings

* Added folding base row + styles tweek

* Commits section: update styles

* UI: commit card layout update

* UI: empty commit message style

* UI and refactor

- Update files list UI
- components rename
- show author for upstream commits

* UI updates

- File list history list UI
- Commit card UI

* UI: Commits footer empty state update
This commit is contained in:
Pavel Laptev 2024-05-26 23:24:53 +02:00 committed by GitHub
parent c553215d5a
commit e7fd4c4457
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 784 additions and 608 deletions

View File

@ -0,0 +1,5 @@
<svg width="52" height="19" viewBox="0 0 52 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.27" d="M6.55211 4.63293L6.94893 3.73981C7.3872 2.7534 8.9086 2.7534 9.12223 3.73981L9.31564 4.63293C9.78195 6.78611 11.3386 8.51755 13.5344 9.32527L13.6717 9.37576C14.6032 9.71842 14.4339 10.9877 13.4243 11.3303C10.9947 12.155 8.98036 13.953 8.04643 16.1631L7.59063 17.2418C7.16345 18.2527 5.60253 18.2527 5.40558 17.2418L5.19543 16.1631C4.76485 13.953 3.15994 12.155 0.918145 11.3303C-0.0133752 10.9877 0.106365 9.71842 1.11592 9.37576L1.26469 9.32527C3.64443 8.51755 5.59544 6.78611 6.55211 4.63293Z" fill="var(--clr-scale-ntrl-60)"/>
<path opacity="0.27" fill-rule="evenodd" clip-rule="evenodd" d="M34 5.78842L23.9212 15.9692L17 8.97796L19.7909 6.15878L23.9212 10.3309L31.2091 2.96924L34 5.78842Z" fill="var(--clr-scale-pop-50)"/>
<path opacity="0.24" fill-rule="evenodd" clip-rule="evenodd" d="M41.1521 7.01939L39.8672 2.16203L42.0557 1.58311L43.3406 6.44047L45.5376 2.40866L47.5559 3.0727L44.9499 7.85514L49.7801 6.57741L50.2601 8.39186L45.4299 9.66959L50.0598 12.5379L48.6338 14.1131L44.7306 11.695L46.0155 16.5523L43.827 17.1312L42.5421 12.2739L40.3451 16.3057L38.3267 15.6417L40.9328 10.8592L36.1026 12.1369L35.6226 10.3225L40.4528 9.04477L35.8229 6.17644L37.2489 4.60127L41.1521 7.01939Z" fill="var(--clr-scale-ntrl-60)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -24,7 +24,7 @@
.avatar {
width: var(--size-16);
height: var(--size-16);
border-radius: var(--radius-m);
border-radius: var(--radius-l);
}
.local {

View File

@ -22,6 +22,8 @@
let mergeUpstreamWarningDismissedCheckbox = false;
$: multiple = base ? base.upstreamCommits.length > 1 || base.upstreamCommits.length == 0 : false;
console.log(base);
</script>
<div class="wrapper">

View File

@ -203,99 +203,101 @@
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
<div
class="branch-card__dropzone-wrapper"
use:dropzone={{
hover: 'move-commit-dz-hover',
active: 'move-commit-dz-active',
accepts: acceptMoveCommit,
onDrop: onCommitDrop,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'cherrypick-dz-hover',
active: 'cherrypick-dz-active',
accepts: acceptCherrypick,
onDrop: onCherrypicked,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'lane-dz-hover',
active: 'lane-dz-active',
accepts: acceptBranchDrop,
onDrop: onBranchDrop,
disabled: isUnapplied
}}
>
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
<DropzoneOverlay class="move-commit-dz-marker" label="Move here" />
<div class="card">
<div
class="branch-card__dropzone-wrapper"
use:dropzone={{
hover: 'move-commit-dz-hover',
active: 'move-commit-dz-active',
accepts: acceptMoveCommit,
onDrop: onCommitDrop,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'cherrypick-dz-hover',
active: 'cherrypick-dz-active',
accepts: acceptCherrypick,
onDrop: onCherrypicked,
disabled: isUnapplied
}}
use:dropzone={{
hover: 'lane-dz-hover',
active: 'lane-dz-active',
accepts: acceptBranchDrop,
onDrop: onBranchDrop,
disabled: isUnapplied
}}
>
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
<DropzoneOverlay class="lane-dz-marker" label="Move here" />
<DropzoneOverlay class="move-commit-dz-marker" label="Move here" />
{#if branch.files?.length > 0}
<div class="card">
<BranchFiles
files={branch.files}
{isUnapplied}
showCheckboxes={$commitBoxOpen}
allowMultiple
bind:this={branchFiles}
/>
{#if branch.active && branch.conflicted}
<div class="card-notifications">
<InfoMessage noRadius filled outlined={false} style="error">
<svelte:fragment slot="title">
{#if branch.files.some((f) => f.conflicted)}
This virtual branch conflicts with upstream changes. Please resolve all
conflicts and commit before you can continue.
{:else}
Please commit your resolved conflicts to continue.
{/if}
</svelte:fragment>
</InfoMessage>
</div>
{/if}
{#if branch.active}
<CommitDialog
projectId={project.id}
expanded={commitBoxOpen}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
{#if branch.files?.length > 0}
<div class="branch-card__files">
<BranchFiles
files={branch.files}
{isUnapplied}
showCheckboxes={$commitBoxOpen}
allowMultiple
bind:this={branchFiles}
/>
{/if}
</div>
{:else if branch.commits.length == 0}
<div class="new-branch card">
<EmptyStatePlaceholder image={laneNewSvg} width="11rem">
<svelte:fragment slot="title">This is a new branch</svelte:fragment>
<svelte:fragment slot="caption">
You can drag and drop files or parts of files here.
</svelte:fragment>
</EmptyStatePlaceholder>
</div>
{:else}
<div class="no-changes card" data-dnd-ignore>
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomShift={false}>
<svelte:fragment slot="caption"
>No uncommitted changes on this branch</svelte:fragment
>
</EmptyStatePlaceholder>
</div>
{/if}
</div>
{#if branch.active && branch.conflicted}
<div class="card-notifications">
<InfoMessage noRadius filled outlined={false} style="error">
<svelte:fragment slot="title">
{#if branch.files.some((f) => f.conflicted)}
This virtual branch conflicts with upstream changes. Please resolve all
conflicts and commit before you can continue.
{:else}
Please commit your resolved conflicts to continue.
{/if}
</svelte:fragment>
</InfoMessage>
</div>
{/if}
<CommitList {isUnapplied} />
<BranchFooter {isUnapplied} />
{#if branch.active}
<CommitDialog
projectId={project.id}
expanded={commitBoxOpen}
on:action={(e) => {
if (e.detail == 'generate-branch-name') {
generateBranchName();
}
}}
/>
{/if}
</div>
{:else if branch.commits.length == 0}
<div class="new-branch">
<EmptyStatePlaceholder image={laneNewSvg} width="11rem">
<svelte:fragment slot="title">This is a new branch</svelte:fragment>
<svelte:fragment slot="caption">
You can drag and drop files or parts of files here.
</svelte:fragment>
</EmptyStatePlaceholder>
</div>
{:else}
<div class="no-changes" data-dnd-ignore>
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomShift={false}>
<svelte:fragment slot="caption"
>No uncommitted changes on this branch</svelte:fragment
>
</EmptyStatePlaceholder>
</div>
{/if}
</div>
<CommitList {isUnapplied} />
<BranchFooter {isUnapplied} />
</div>
</div>
</ScrollableContainer>
<div class="divider-line">
<Resizer
viewport={rsViewport}
direction="right"
minWidth={340}
minWidth={380}
sticky
defaultLineColor={$fileIdSelection.length == 1 ? 'transparent' : 'var(--clr-border-2)'}
on:width={(e) => {
@ -349,6 +351,13 @@
.card {
flex: 1;
overflow: hidden;
}
.branch-card__files {
display: flex;
flex-direction: column;
flex: 1;
}
.card-notifications {

View File

@ -31,7 +31,7 @@
role="listbox"
tabindex="-1"
on:keydown={(e) => {
if (e.key === 'Escape') {
if (e.key == 'Escape') {
unselectAllFiles();
}
}}
@ -53,7 +53,6 @@
.branch-files {
flex: 1;
background: var(--clr-bg-1);
border-radius: var(--radius-m) var(--radius-m) 0 0;
padding: 0 var(--size-14) var(--size-14);
/* padding: 0 var(--size-14) var(--size-14); */
}
</style>

View File

@ -6,6 +6,7 @@
import type { AnyFile } from '$lib/vbranches/types';
import type { Writable } from 'svelte/store';
export let title: string;
export let files: AnyFile[];
export let showCheckboxes = false;
@ -58,7 +59,7 @@
/>
{/if}
<div class="header__title text-base-13 text-semibold">
<span>Changes</span>
<span>{title}</span>
<Badge count={files.length} />
</div>
</div>
@ -69,8 +70,7 @@
display: flex;
align-items: center;
justify-content: space-between;
padding-top: var(--size-14);
padding-bottom: var(--size-14);
padding: var(--size-14);
}
.header__title {
display: flex;

View File

@ -9,6 +9,7 @@
import { sortLikeFileTree } from '$lib/vbranches/filetree';
import type { AnyFile } from '$lib/vbranches/types';
export let title: string = 'Changes';
export let files: AnyFile[];
export let isUnapplied = false;
export let showCheckboxes = false;
@ -47,7 +48,7 @@
}
</script>
<BranchFilesHeader {files} {showCheckboxes} />
<BranchFilesHeader {title} {files} {showCheckboxes} />
{#each displayedFiles as file (file.id)}
<FileListItem
{file}

View File

@ -1,6 +1,7 @@
<script lang="ts">
import PassphraseBox from './PassphraseBox.svelte';
import PushButton, { BranchAction } from './PushButton.svelte';
import emptyStateImg from '$lib/assets/empty-state/commits-up-to-date.svg?raw';
import { PromptService } from '$lib/backend/prompt';
import { getContext, getContextStore } from '$lib/utils/context';
import { BranchController } from '$lib/vbranches/branchController';
@ -56,7 +57,15 @@
}}
/>
{:else}
Branch {$branch.name} is up to date with the remote.
<div class="empty-state">
<span class="text-base-body-12 empty-state__text"
>Your branch is up to date with the remote.</span
>
<i class="empty-state__image">
{@html emptyStateImg}
</i>
</div>
{/if}
</div>
{/if}
@ -65,7 +74,24 @@
.actions {
background: var(--clr-bg-1);
padding: var(--size-16);
border-radius: 0 0 var(--radius-m) var(--radius-m);
border: 1px solid var(--clr-border-2);
}
/* EMPTY STATE */
.empty-state {
display: flex;
/* justify-content: space-between; */
align-items: center;
gap: var(--size-20);
}
.empty-state__image {
flex-shrink: 0;
}
.empty-state__text {
color: var(--clr-text-3);
flex: 1;
/* max-width: 8rem; */
}
</style>

View File

@ -45,7 +45,6 @@
by {branch.author?.name ?? 'unknown'}
{/if}
</span>
<!-- <AuthorIcons authors={branch.authors} /> -->
</div>
{/if}
</div>

View File

@ -102,7 +102,7 @@
<Resizer
viewport={rsViewport}
direction="right"
minWidth={320}
minWidth={400}
defaultLineColor="var(--clr-border-2)"
on:width={(e) => {
fileWidth = e.detail / (16 * $userSettings.zoom);

View File

@ -1,5 +1,6 @@
<script lang="ts">
import BranchFilesList from './BranchFilesList.svelte';
import CommitDragItem from './CommitDragItem.svelte';
import Icon from './Icon.svelte';
import { Project } from '$lib/backend/projects';
import Button from '$lib/components/Button.svelte';
@ -11,6 +12,7 @@
import { draggable } from '$lib/dragging/draggable';
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
import { getContext, getContextStore } from '$lib/utils/context';
import { getTimeAgo } from '$lib/utils/timeAgo';
import { openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController';
import { createCommitStore, getSelectedFiles } from '$lib/vbranches/contexts';
@ -24,7 +26,8 @@
BaseBranch,
type CommitStatus
} from '$lib/vbranches/types';
import { slide } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
// import { slide } from 'svelte/transition';
export let branch: Branch | undefined = undefined;
export let commit: Commit | RemoteCommit;
@ -47,8 +50,10 @@
const currentCommitMessage = persistedCommitMessage(project.id, branch?.id || '');
let showFiles = false;
const dispatch = createEventDispatcher<{ toggle: void }>();
let files: RemoteFile[] = [];
let showDetails = false;
$: selectedFile =
$fileIdSelection.length == 1 &&
@ -61,9 +66,10 @@
}
function toggleFiles() {
showFiles = !showFiles;
showDetails = !showDetails;
dispatch('toggle');
if (showFiles) loadFiles();
if (showDetails) loadFiles();
}
function onKeyup(e: KeyboardEvent) {
@ -143,176 +149,181 @@
</Modal>
<div
use:draggable={commit instanceof Commit
? {
data: new DraggableCommit(commit.branchId, commit, isHeadCommit)
}
: nonDraggable()}
class="commit"
class:is-commit-open={showFiles}
class="commit-row"
class:is-commit-open={showDetails}
class:is-first={first}
class:is-last={last}
>
<div
class="accent"
class:is-first={first}
class:is-last={last}
class:local={type == 'local'}
class:remote={type == 'remote'}
class:upstream={type == 'upstream'}
></div>
<slot name="lines" />
<CommitDragItem {commit}>
<div
use:draggable={commit instanceof Commit
? {
data: new DraggableCommit(commit.branchId, commit, isHeadCommit)
}
: nonDraggable()}
class="commit-card"
class:is-first={first}
class:is-last={last}
>
<div
class="accent-border-line"
class:is-first={first}
class:is-last={last}
class:local={type == 'local'}
class:remote={type == 'remote'}
class:upstream={type == 'upstream'}
/>
<div class="commit__header" on:click={toggleFiles} on:keyup={onKeyup} role="button" tabindex="0">
{#if first}
<div class="commit__type text-semibold text-base-12">
{#if type == 'remote'}
Local and remote <Icon name="local-remote" />
{:else if type == 'local'}
Local <Icon name="local" />
{:else if type == 'upstream'}
Remote upstream <Icon name="remote" />
{/if}
</div>
{/if}
<div class="commit__message">
{#if $advancedCommitOperations}
<div class="commit__id">
<code>
{#if commit.isSigned}
<span class="text-xs">🔒</span>
{/if}
{#if commit.changeId}
{commit.changeId.split('-')[0]}
{:else}
{commit.id.substring(0, 6)}
{/if}
</code>
</div>
{/if}
<div class="commit__row">
{#if isUndoable}
{#if commit.descriptionTitle}
<span class="commit__title text-semibold text-base-12" class:truncate={!showFiles}>
{commit.descriptionTitle}
</span>
<div class="commit__content">
<!-- GENERAL INFO -->
<div
class="commit__about"
on:click={toggleFiles}
on:keyup={onKeyup}
role="button"
tabindex="0"
>
{#if first}
<div class="commit__type text-semibold text-base-12">
{#if type == 'remote'}
Local and remote
{:else if type == 'local'}
Local <Icon name="local" />
{:else if type == 'upstream'}
Remote upstream <Icon name="remote" />
{/if}
</div>
{/if}
{#if isUndoable && !commit.descriptionTitle}
<span class="text-base-body-13 text-semibold commit__empty-title"
>empty commit message</span
>
{:else}
<span
class="commit__title_no_desc text-base-12 text-zinc-400"
class:truncate={!showFiles}
>
<i>empty commit message</i>
</span>
<h5 class="text-base-body-13 text-semibold commit__title" class:truncate={!showDetails}>
{commit.descriptionTitle}
</h5>
{#if $advancedCommitOperations}
<div class="text-base-11 commit__subtitle">
<span class="commit__id">
{#if commit.changeId}
{commit.changeId.split('-')[0]}
{:else}
{commit.id.substring(0, 6)}
{/if}
{#if commit.isSigned}
<Icon name="locked-small" />
{/if}
</span>
<span class="commit__subtitle-divider"></span>
<span
>{getTimeAgo(commit.createdAt)}{type == 'remote' || type == 'upstream'
? ` by ${commit.author.name}`
: ''}</span
>
</div>
{/if}
{/if}
{#if !showFiles}
<Tag
style="ghost"
kind="solid"
icon="undo-small"
clickable
on:click={(e) => {
currentCommitMessage.set(commit.description);
e.stopPropagation();
undoCommit(commit);
}}>Undo</Tag
>
{/if}
{:else}
<span class="commit__title text-base-12" class:truncate={!showFiles}>
{commit.descriptionTitle}
</span>
{/if}
</div>
{#if showFiles}
{#if commit.descriptionBody}
<div class="commit__row" transition:slide={{ duration: 100 }}>
<span class="commit__body text-base-body-12">
{commit.descriptionBody}
</span>
</div>
<!-- HIDDEN -->
{#if showDetails}
<div class="commit__details">
{#if hasCommitUrl || isUndoable}
<div class="commit__actions hide-native-scrollbar">
{#if isUndoable}
<Tag
style="ghost"
kind="solid"
icon="undo-small"
clickable
on:click={(e) => {
currentCommitMessage.set(commit.description);
e.stopPropagation();
undoCommit(commit);
}}>Undo</Tag
>
{#if $advancedCommitOperations}
<Tag
style="ghost"
kind="solid"
icon="edit-text"
clickable
on:click={openCommitMessageModal}>Edit message</Tag
>
<Tag
style="ghost"
kind="solid"
clickable
on:click={(e) => {
e.stopPropagation();
reorderCommit(commit, -1);
}}>Move Up</Tag
>
<Tag
style="ghost"
kind="solid"
clickable
on:click={(e) => {
e.stopPropagation();
reorderCommit(commit, 1);
}}>Move Down</Tag
>
<Tag
style="ghost"
kind="solid"
clickable
on:click={(e) => {
e.stopPropagation();
insertBlankCommit(commit, -1);
}}>Add Before</Tag
>
<Tag
style="ghost"
kind="solid"
clickable
on:click={(e) => {
e.stopPropagation();
insertBlankCommit(commit, 1);
}}>Add After</Tag
>
{/if}
{/if}
{#if hasCommitUrl}
<Tag
style="ghost"
kind="solid"
icon="open-link"
clickable
on:click={() => {
if (commitUrl) openExternalUrl(commitUrl);
}}>Open</Tag
>
{/if}
</div>
{/if}
{#if commit.descriptionBody}
<span class="commit__description text-base-body-12">
{commit.descriptionBody}
</span>
{/if}
</div>
{/if}
</div>
{#if $advancedCommitOperations && isUndoable}
<Tag clickable on:click={openCommitMessageModal}>Edit</Tag>
{/if}
{#if showDetails}
<div class="files-container">
<BranchFilesList title="Files" {files} {isUnapplied} />
</div>
{/if}
</div>
<!-- <span class="commit__time text-base-11">
<TimeAgo date={commit.createdAt} />
</span> -->
</div>
{#if showFiles}
<div class="files-container" transition:slide={{ duration: 100 }}>
<BranchFilesList {files} {isUnapplied} />
</div>
{#if hasCommitUrl || isUndoable}
<div class="files__footer">
{#if isUndoable}
{#if $advancedCommitOperations}
<Tag
style="ghost"
kind="solid"
clickable
on:click={(e) => {
e.stopPropagation();
reorderCommit(commit, -1);
}}>Move Up</Tag
>
<Tag
style="ghost"
kind="solid"
clickable
on:click={(e) => {
e.stopPropagation();
reorderCommit(commit, 1);
}}>Move Down</Tag
>
<Tag
style="ghost"
kind="solid"
clickable
on:click={(e) => {
e.stopPropagation();
insertBlankCommit(commit, -1);
}}>Add Before</Tag
>
<Tag
style="ghost"
kind="solid"
clickable
on:click={(e) => {
e.stopPropagation();
insertBlankCommit(commit, 1);
}}>Add After</Tag
>
{/if}
<Tag
style="ghost"
kind="solid"
icon="undo-small"
clickable
on:click={(e) => {
currentCommitMessage.set(commit.description);
e.stopPropagation();
undoCommit(commit);
}}>Undo</Tag
>
{/if}
{#if hasCommitUrl}
<Tag
style="ghost"
kind="solid"
icon="open-link"
clickable
on:click={() => {
if (commitUrl) openExternalUrl(commitUrl);
}}>Open commit</Tag
>
{/if}
</div>
{/if}
{/if}
</CommitDragItem>
</div>
<style lang="postcss">
@ -324,33 +335,50 @@
visibility: visible;
}
.commit {
.commit-row {
position: relative;
display: flex;
gap: var(--size-8);
padding-right: var(--size-14);
/* border-top: 1px solid var(--clr-border-2); */
/* padding-left: var(--size-8); */
&:not(.is-first) {
border-top: 1px solid var(--clr-border-3);
}
}
.commit-card {
display: flex;
position: relative;
flex-direction: column;
background-color: var(--clr-bg-1);
border: 1px solid var(--clr-border-2);
/* border: 1px solid var(--clr-border-2);
border-top: none;
border-bottom: none;
border-left: none; */
border-right: 1px solid var(--clr-border-2);
overflow: hidden;
transition: background-color var(--transition-fast);
&.is-first {
margin-top: var(--size-12);
border-top: 1px solid var(--clr-border-2);
border-top-left-radius: var(--radius-m);
border-top-right-radius: var(--radius-m);
}
&.is-last {
border-bottom: 1px solid var(--clr-border-2);
border-bottom-left-radius: var(--radius-m);
border-bottom-right-radius: var(--radius-m);
}
&:not(.is-first) {
border-top: none;
}
&:not(.is-commit-open):hover {
background-color: var(--clr-bg-2);
}
}
.accent {
.accent-border-line {
position: absolute;
width: var(--size-4);
height: 100%;
@ -363,99 +391,114 @@
&.upstream {
background-color: var(--clr-commit-upstream);
}
&.is-first {
border-top-left-radius: var(--radius-m);
}
&.is-last {
border-bottom-left-radius: var(--radius-m);
}
}
.commit__header {
cursor: pointer;
display: flex;
flex-direction: column;
gap: var(--size-10);
padding: var(--size-14);
}
.commit__type {
opacity: 0.4;
}
.is-commit-open {
background-color: var(--clr-bg-2);
& .commit__header {
padding-bottom: var(--size-16);
border-bottom: 1px solid var(--clr-border-2);
}
& .commit__message {
margin-bottom: var(--size-4);
}
/* HEADER */
.commit__content {
display: flex;
flex-direction: column;
}
.commit__message {
.commit__about {
display: flex;
flex-direction: column;
gap: var(--size-6);
padding: var(--size-14);
&:hover {
background-color: var(--clr-bg-1-muted);
}
}
.commit__title {
flex: 1;
display: block;
color: var(--clr-scale-ntrl-0);
width: 100%;
}
.commit__title_no_desc {
flex: 1;
display: block;
color: var(--clr-text-1);
width: 100%;
}
.commit__body {
flex: 1;
display: block;
width: 100%;
color: var(--clr-scale-ntrl-40);
white-space: pre-line;
word-wrap: anywhere;
.commit__description {
color: var(--clr-text-2);
}
.commit__row {
.commit__empty-title {
color: var(--clr-text-3);
}
.commit__subtitle {
display: flex;
align-items: center;
gap: var(--size-8);
flex-wrap: nowrap;
gap: var(--size-4);
color: var(--clr-text-2);
overflow: hidden;
& > span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.commit__id {
display: flex;
align-items: center;
justify-content: center;
margin-top: -14px;
}
.commit__id > code {
background-color: #eeeeee;
padding: 1px 12px;
color: #888888;
font-size: x-small;
border-radius: 0px 0px 6px 6px;
margin-bottom: -8px;
gap: var(--size-4);
}
.files-container {
background-color: var(--clr-bg-1);
padding: 0 var(--size-14) var(--size-14);
.commit__subtitle-divider {
opacity: 0.4;
}
.files__footer {
/* DETAILS */
.commit__details {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: var(--size-8);
flex-direction: column;
gap: var(--size-12);
padding: var(--size-14);
background-color: var(--clr-bg-1);
border-top: 1px solid var(--clr-border-2);
}
.commit__actions {
display: flex;
gap: var(--size-4);
overflow-x: auto;
margin: 0 calc(var(--size-14) * -1);
padding: 0 var(--size-14);
}
/* FILES */
.files-container {
border-top: 1px solid var(--clr-border-2);
}
/* MODIFIERS */
.is-commit-open {
&:not(.is-first) {
border-top: 1px solid var(--clr-border-3);
& .commit-card {
margin-top: var(--size-8);
}
}
& .commit-card {
border-top: 1px solid var(--clr-border-2);
border-bottom: 1px solid var(--clr-border-2);
border-radius: var(--radius-m);
}
&:not(.is-last) .commit-card {
margin-bottom: var(--size-8);
}
& .commit__about {
background-color: var(--clr-bg-1-muted);
}
}
</style>

View File

@ -92,7 +92,6 @@
background: var(--clr-bg-1);
border-top: 1px solid var(--clr-border-2);
transition: background-color var(--transition-medium);
border-radius: 0 0 var(--radius-m) var(--radius-m);
}
.commit-box__expander {

View File

@ -17,7 +17,7 @@
export let base = false;
</script>
<div class="lines" class:base>
<div class="lines">
{#if hasShadowColumn}
<ShadowLine
line={shadowLine}
@ -42,8 +42,10 @@
(!!remoteCommit && !remoteCommit?.children?.[0])}
{base}
/>
{#if hasLocalColumn}
<LocalLine
isEmpty={base}
commit={localCommit?.status == 'local' ? localCommit : undefined}
dashed={localLine}
{first}
@ -55,9 +57,8 @@
.lines {
display: flex;
align-items: stretch;
min-height: var(--size-16);
&.base {
height: var(--size-40);
}
min-height: var(--size-12);
padding-left: var(--size-8);
/* margin-top: -1px; */
}
</style>

View File

@ -1,10 +1,12 @@
<script lang="ts">
import CommitCard from './CommitCard.svelte';
import CommitLines from './CommitLines.svelte';
import CommitListItem from './CommitListItem.svelte';
import { Project } from '$lib/backend/projects';
import { getContext } from '$lib/utils/context';
import { getContextStore } from '$lib/utils/context';
import { getLocalCommits, getRemoteCommits, getUnknownCommits } from '$lib/vbranches/contexts';
import { BaseBranch, Branch } from '$lib/vbranches/types';
import { goto } from '$app/navigation';
export let isUnapplied: boolean;
@ -13,22 +15,37 @@
const remoteCommits = getRemoteCommits();
const unknownCommits = getUnknownCommits();
const baseBranch = getContextStore(BaseBranch);
const project = getContext(Project);
$: hasShadowColumn = $localCommits.some((c) => c.relatedTo && c.id != c.relatedTo.id);
$: hasLocalColumn = $localCommits.length > 0;
$: hasCommits = $branch.commits && $branch.commits.length > 0;
$: headCommit = $branch.commits.at(0);
$: hasUnknownCommits = $unknownCommits.length > 0;
$: baseCommit = $baseBranch.recentCommits.at($baseBranch.recentCommits.length - 1)?.id;
let baseIsUnfolded = false;
</script>
{#if hasCommits || hasUnknownCommits}
<div class="commit-list__content">
<div class="title text-base-13 text-semibold"></div>
<div class="commits">
{#if $unknownCommits.length > 0}
<CommitLines {hasShadowColumn} {hasLocalColumn} localLine />
{#each $unknownCommits as commit, idx (commit.id)}
<div class="commit-lines">
<div class="commits">
<!-- UPSTREAM COMMITS -->
{#if $unknownCommits.length > 0}
{#each $unknownCommits as commit, idx (commit.id)}
<CommitCard
type="upstream"
branch={$branch}
{commit}
{isUnapplied}
first={idx == 0}
last={idx == $unknownCommits.length - 1}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
on:toggle={() => {
console.log('toggle upstream');
}}
>
<svelte:fragment slot="lines">
<CommitLines
{hasLocalColumn}
{hasShadowColumn}
@ -37,30 +54,27 @@
remoteCommit={commit}
first={idx == 0}
/>
<CommitListItem {commit}>
<CommitCard
type="upstream"
branch={$branch}
{commit}
{isUnapplied}
first={idx == 0}
last={idx == $unknownCommits.length - 1}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
/>
</CommitListItem>
</div>
{/each}
{/if}
{#if $localCommits.length > 0}
<CommitLines
{hasShadowColumn}
{hasLocalColumn}
upstreamLine={hasUnknownCommits}
localLine
/>
{#each $localCommits as commit, idx (commit.id)}
<div class="commit-lines">
</svelte:fragment>
</CommitCard>
{/each}
{/if}
<!-- LOCAL COMMITS -->
{#if $localCommits.length > 0}
{#each $localCommits as commit, idx (commit.id)}
<CommitCard
branch={$branch}
{commit}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
{isUnapplied}
first={idx == 0}
last={idx == $localCommits.length - 1}
type="local"
on:toggle={() => {
console.log('toggle local');
}}
>
<svelte:fragment slot="lines">
<CommitLines
{hasLocalColumn}
{hasShadowColumn}
@ -69,30 +83,28 @@
first={idx == 0}
upstreamLine={hasUnknownCommits}
/>
<CommitListItem {commit}>
<CommitCard
branch={$branch}
{commit}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
{isUnapplied}
first={idx == 0}
last={idx == $localCommits.length - 1}
type="local"
/>
</CommitListItem>
</div>
{/each}
{/if}
{#if $remoteCommits.length > 0}
<CommitLines
{hasShadowColumn}
{hasLocalColumn}
upstreamLine={hasUnknownCommits}
localLine
/>
{#each $remoteCommits as commit, idx (commit.id)}
<div class="commit-lines">
</svelte:fragment>
</CommitCard>
<!-- </div> -->
{/each}
{/if}
<!-- REMOTE COMMITS -->
{#if $remoteCommits.length > 0}
{#each $remoteCommits as commit, idx (commit.id)}
<CommitCard
branch={$branch}
{commit}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
{isUnapplied}
first={idx == 0}
last={idx == $remoteCommits.length - 1}
type="remote"
on:toggle={() => {
console.log('toggle remote');
}}
>
<svelte:fragment slot="lines">
<CommitLines
{hasLocalColumn}
{hasShadowColumn}
@ -101,45 +113,118 @@
first={idx == 0}
upstreamLine={hasUnknownCommits}
/>
<CommitListItem {commit}>
<CommitCard
branch={$branch}
{commit}
commitUrl={$baseBranch?.commitUrl(commit.id)}
isHeadCommit={commit.id === headCommit?.id}
{isUnapplied}
first={idx == 0}
last={idx == $remoteCommits.length - 1}
type="remote"
/>
</CommitListItem>
</div>
{/each}
{/if}
<CommitLines
{hasShadowColumn}
localLine={$remoteCommits.length == 0 && $localCommits.length > 0}
localRoot={$remoteCommits.length == 0 && $localCommits.length > 0}
remoteLine={$remoteCommits.length > 0}
shadowLine={hasShadowColumn}
base
/>
</svelte:fragment>
</CommitCard>
{/each}
{/if}
<!-- BASE -->
<div class="base-row-container" class:base-row-container_unfolded={baseIsUnfolded}>
<div
class="commit-group base-row"
tabindex="0"
role="button"
on:click|stopPropagation={() => (baseIsUnfolded = !baseIsUnfolded)}
on:keydown={(e) => e.key === 'Enter' && (baseIsUnfolded = !baseIsUnfolded)}
>
<div class="base-row__lines">
<CommitLines
{hasShadowColumn}
localLine={$remoteCommits.length == 0 && $localCommits.length > 0}
localRoot={$remoteCommits.length == 0 && $localCommits.length > 0}
remoteLine={$remoteCommits.length > 0}
shadowLine={hasShadowColumn}
{hasLocalColumn}
base
/>
</div>
<div class="base-row__content">
<span class="text-base-11 base-row__text"
>Base commit <button
class="base-row__commit-link"
on:click={async () => await goto(`/${project.id}/base`)}
>
{baseCommit ? baseCommit.slice(0, 7) : ''}
</button>
</span>
</div>
</div>
</div>
</div>
{/if}
<style lang="postcss">
.commit-lines {
display: flex;
}
.commit-list__content {
display: flex;
flex-direction: column;
}
.commits {
display: flex;
flex-direction: column;
background-color: var(--clr-bg-2);
border-top: 1px solid var(--clr-border-2);
border-bottom: 1px solid var(--clr-border-2);
--base-top-margin: var(--size-8);
--base-icon-top: var(--size-16);
--base-unfolded: var(--size-48);
--avatar-first-top: 3.1rem;
--avatar-top: var(--size-16);
}
.commit-group {
/* padding-right: var(--size-14);
padding-left: var(--size-8); */
}
/* BASE ROW */
.base-row-container {
display: flex;
flex-direction: column;
height: var(--size-20);
overflow: hidden;
transition: height var(--transition-medium);
}
.base-row-container_unfolded {
height: var(--base-unfolded);
--base-icon-top: var(--size-20);
& .base-row__text {
opacity: 1;
}
}
.base-row {
display: flex;
gap: var(--size-8);
border-top: 1px solid var(--clr-border-3);
min-height: calc(var(--base-unfolded) - var(--base-top-margin));
margin-top: var(--base-top-margin);
transition: background-color var(--transition-fast);
&:hover {
background-color: var(--clr-bg-2-muted);
}
}
.base-row__lines {
display: flex;
margin-top: calc(var(--size-8) * -1);
}
.base-row__content {
display: flex;
align-items: center;
}
.base-row__text {
color: var(--clr-text-2);
opacity: 0;
margin-top: var(--size-2);
transition: opacity var(--transition-medium);
}
.base-row__commit-link {
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@ -59,7 +59,60 @@
});
</script>
<div class:list-item-wrapper={showCheckbox}>
<div
bind:this={draggableElt}
class="file-list-item"
class:selected-draggable={selected}
id={`file-${file.id}`}
data-locked={file.locked}
on:click
on:keydown
on:dragstart={() => {
// Reset selection if the file being dragged is not in the selected list
if ($fileIdSelection.length > 0 && !fileIdSelection.has(file.id, $commit?.id)) {
fileIdSelection.clear();
fileIdSelection.add(file.id, $commit?.id);
}
if ($selectedFiles.length > 0) {
$selectedFiles.forEach((f) => {
if (f.locked) {
const lockedElement = document.getElementById(`file-${f.id}`);
if (lockedElement) {
// add a class to the locked file
lockedElement.classList.add('locked-file-animation');
}
}
});
} else if (file.locked) {
draggableElt.classList.add('locked-file-animation');
}
}}
on:animationend={() => {
// remove the class after the animation ends
if (file.locked) {
draggableElt.classList.remove('locked-file-animation');
}
}}
use:draggable={{
data: new DraggableFile($branch?.id || '', file, $commit, selectedFiles),
disabled: readonly || isUnapplied,
viewportId: 'board-viewport',
selector: '.selected-draggable'
}}
role="button"
tabindex="0"
on:contextmenu|preventDefault={(e) => {
const files = fileIdSelection.has(file.id, $commit?.id)
? $fileIdSelection
.map((key) => $selectedFiles?.find((f) => fileKey(f.id, $commit?.id) == key))
.filter(isDefined)
: [file];
if (files.length > 0) popupMenu.openByMouse(e, { files });
else console.error('No files selected');
}}
>
{#if showCheckbox}
<Checkbox
small
@ -74,97 +127,39 @@
}}
/>
{/if}
<div
bind:this={draggableElt}
class="file-list-item"
class:selected-draggable={selected}
id={`file-${file.id}`}
data-locked={file.locked}
on:click
on:keydown
on:dragstart={() => {
// Reset selection if the file being dragged is not in the selected list
if ($fileIdSelection.length > 0 && !fileIdSelection.has(file.id, $commit?.id)) {
fileIdSelection.clear();
fileIdSelection.add(file.id, $commit?.id);
}
if ($selectedFiles.length > 0) {
$selectedFiles.forEach((f) => {
if (f.locked) {
const lockedElement = document.getElementById(`file-${f.id}`);
if (lockedElement) {
// add a class to the locked file
lockedElement.classList.add('locked-file-animation');
}
}
});
} else if (file.locked) {
draggableElt.classList.add('locked-file-animation');
}
}}
on:animationend={() => {
// remove the class after the animation ends
if (file.locked) {
draggableElt.classList.remove('locked-file-animation');
}
}}
use:draggable={{
data: new DraggableFile($branch?.id || '', file, $commit, selectedFiles),
disabled: readonly || isUnapplied,
viewportId: 'board-viewport',
selector: '.selected-draggable'
}}
role="button"
tabindex="0"
on:contextmenu|preventDefault={(e) => {
const files = fileIdSelection.has(file.id, $commit?.id)
? $fileIdSelection
.map((key) => $selectedFiles?.find((f) => fileKey(f.id, $commit?.id) == key))
.filter(isDefined)
: [file];
if (files.length > 0) popupMenu.openByMouse(e, { files });
else console.error('No files selected');
}}
>
<div class="info">
<img draggable="false" class="file-icon" src={getVSIFileIcon(file.path)} alt="" />
<span class="text-base-12 name">
{file.filename}
</span>
<span class="text-base-12 path">
{file.justpath}
</span>
</div>
<FileStatusIcons {file} />
<div class="info">
<img draggable="false" class="file-icon" src={getVSIFileIcon(file.path)} alt="" />
<span class="text-base-12 name">
{file.filename}
</span>
<span class="text-base-12 path">
{file.justpath}
</span>
</div>
<FileStatusIcons {file} />
</div>
<style lang="postcss">
.list-item-wrapper {
display: flex;
align-items: center;
gap: var(--size-8);
}
.file-list-item {
flex: 1;
display: flex;
align-items: center;
height: var(--size-28);
padding: var(--size-4) var(--size-8);
gap: var(--size-16);
border-radius: var(--radius-s);
padding: var(--size-6) var(--size-14);
gap: var(--size-10);
height: 2rem;
overflow: hidden;
text-align: left;
user-select: none;
outline: none;
background: var(--clr-bg-1);
border: 1px solid transparent;
border-bottom: 1px solid var(--clr-border-3);
&:not(.selected-draggable):hover {
background-color: var(--clr-bg-2);
background-color: var(--clr-bg-1-muted);
}
&:last-child {
border-bottom: none;
}
}
@ -180,6 +175,7 @@
.file-icon {
width: var(--size-12);
}
.name {
color: var(--clr-scale-ntrl-0);
white-space: nowrap;
@ -188,6 +184,7 @@
overflow: hidden;
line-height: 120%;
}
.path {
color: var(--clr-scale-ntrl-0);
line-height: 120%;
@ -198,12 +195,9 @@
opacity: 0.3;
}
.selected-draggable {
background-color: var(--clr-scale-pop-80);
border: 1px solid var(--clr-bg-1);
/* MODIFIERS */
&:hover {
background-color: var(--clr-scale-pop-80);
}
.selected-draggable {
background-color: var(--clr-theme-pop-bg);
}
</style>

View File

@ -6,6 +6,7 @@
export let dashed: boolean;
export let commit: Commit | undefined;
export let first: boolean;
export let isEmpty: boolean = false;
$: hasRoot = isRoot(commit);
$: tooltipText = getAvatarTooltip(commit);
@ -16,36 +17,39 @@
</script>
<div class="local-column">
{#if !commit && dashed}
<div class="local-line dashed"></div>
{:else if commit}
{#if first}
<div class="local-line dashed tip" />
{#if !isEmpty}
{#if !commit && dashed}
<div class="local-line dashed"></div>
{:else if commit}
{#if first}
<div class="local-line dashed tip" />
{/if}
<div class="local-line" class:has-root={hasRoot} class:short={first} />
{/if}
{#if commit}
{@const author = commit.author}
<div class="avatar" class:first>
<Avatar {author} status={commit.status} help={tooltipText} />
</div>
{/if}
{#if hasRoot}
<div class="root" class:long-root={commit?.parent} />
{/if}
<div class="local-line" class:has-root={hasRoot} class:short={first} />
{/if}
{#if commit}
{@const author = commit.author}
<div class="avatar" class:first>
<Avatar {author} status={commit.status} help={tooltipText} />
</div>
{/if}
{#if hasRoot}
<div class="root" class:long-root={commit?.parent} />
{/if}
</div>
<style lang="postcss">
.local-column {
position: relative;
width: var(--size-16);
width: var(--size-14);
/* background-color: rgba(255, 228, 196, 0.46); */
}
.avatar {
position: absolute;
top: var(--size-12);
left: calc(-1 * var(--size-4));
top: var(--avatar-top);
left: -0.188rem;
&.first {
top: 2.625rem;
top: var(--avatar-first-top);
}
}
@ -79,14 +83,14 @@
.root {
position: absolute;
width: var(--size-10);
top: calc(100% - var(--size-12));
top: calc(100% - var(--size-14));
left: calc(-1 * var(--size-4));
bottom: calc(-1 * var(--size-2));
border-radius: 0 0 var(--radius-l) 0;
border-color: var(--clr-commit-local);
border-width: 0 var(--size-2) var(--size-2) 0;
&.long-root {
bottom: -3rem;
bottom: -2rem;
}
}
</style>

View File

@ -15,13 +15,13 @@
$: tooltipText = getAvatarTooltip(commit || remoteCommit);
</script>
<div class="remote-column" class:has-root={root}>
<div class="remote-column" class:has-root={root} class:base>
{#if base}
<div class="remote-line dashed" class:short={!line} />
{#if root}
<div class="root base" />
{/if}
<div class="commit-icon">
<div class="base-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
@ -68,6 +68,11 @@
.remote-column {
position: relative;
width: var(--size-24);
/* background-color: rgba(125, 138, 154, 0.307); */
/* &.base {
margin-top: calc(var(--size-8) * -1);
} */
}
.remote-line {
@ -81,9 +86,9 @@
top: calc(var(--size-40) + var(--size-2));
}
&.short {
top: 1rem;
top: var(--avatar-top);
&.first {
top: 3rem;
top: var(--avatar-first-top);
}
}
&.tip {
@ -102,9 +107,9 @@
background-color: var(--clr-commit-upstream);
top: 0;
&.short {
top: 1rem;
top: var(--avatar-top);
&.first {
top: calc(var(--size-40) + var(--size-2));
top: var(--avatar-first-top);
}
}
}
@ -112,10 +117,10 @@
.avatar {
position: absolute;
top: var(--size-10);
top: var(--avatar-top);
left: var(--size-4);
&.first {
top: calc(var(--size-40) + var(--size-2));
top: var(--avatar-first-top);
}
}
@ -124,24 +129,29 @@
width: var(--size-10);
top: 1.875rem;
border-radius: var(--radius-l) 0 0 0;
height: var(--size-10);
height: var(--size-16);
left: calc(var(--size-10) + var(--size-1));
border-color: var(--clr-commit-local);
border-width: var(--size-2) 0 0 var(--size-2);
&.base {
top: 0;
top: -1px;
}
}
.commit-icon {
display: inline-block;
.base-icon {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
border-radius: 6px;
left: var(--size-4);
top: var(--base-icon-top);
left: 50%;
transform: translateX(-50%);
background: var(--clr-commit-remote);
height: var(--size-16);
width: var(--size-16);
top: var(--size-10);
height: 1.125rem;
width: 1.125rem;
transition: top var(--transition-medium);
& svg {
height: var(--size-16);
width: var(--size-16);

View File

@ -1,5 +1,4 @@
<script lang="ts">
import Avatar from './Avatar.svelte';
import { getAvatarTooltip } from '$lib/utils/avatar';
import { tooltip } from '$lib/utils/tooltip';
import type { Commit, RemoteCommit } from '$lib/vbranches/types';
@ -27,12 +26,6 @@
{#if localCommit}
<div class="shadow-marker" class:first class:short use:tooltip={tooltipText}></div>
{/if}
{#if remoteCommit}
{@const author = remoteCommit.author}
<div class="avatar" class:first class:short>
<Avatar {author} status={remoteCommit.status} help={tooltipText} />
</div>
{/if}
</div>
<style lang="postcss">
@ -52,9 +45,9 @@
bottom: 0;
top: 0;
&.short {
top: 1rem;
top: calc(var(--avatar-top) + var(--size-2));
&.first {
top: 3rem;
top: calc(var(--avatar-first-top) + var(--size-2));
}
}
&.dashed {
@ -80,19 +73,10 @@
height: var(--size-10);
border-radius: 100%;
background-color: var(--clr-commit-shadow);
top: var(--size-14);
top: calc(var(--avatar-top) + var(--size-2));
left: 50%;
&.first {
top: 2.75rem;
}
}
.avatar {
position: absolute;
top: var(--size-10);
left: var(--size-4);
&.first {
top: calc(var(--size-40) + var(--size-2));
top: calc(var(--avatar-first-top) + var(--size-2));
}
}
</style>

View File

@ -4,7 +4,7 @@
export let foldable: boolean = false;
export let foldedAmount: number | undefined = undefined;
export let foldedHeight = '2.6rem';
export let foldedHeight = '3rem';
let isOpen: boolean = false;
let el: HTMLElement;

View File

@ -303,7 +303,7 @@
position: absolute;
top: var(--size-24);
content: '';
height: calc(100% - var(--size-12));
height: calc(100% - var(--size-14));
min-height: var(--size-8);
width: 1px;
background-color: var(--clr-border-2);
@ -356,23 +356,26 @@
.files-attacment {
display: flex;
flex-direction: column;
gap: var(--size-2);
padding: var(--size-4);
}
.files-attacment__file {
display: flex;
align-items: center;
gap: var(--size-6);
padding: var(--size-4);
padding: var(--size-8);
border-bottom: 1px solid var(--clr-border-3);
&:not(.file-selected):hover {
background-color: var(--clr-bg-1-muted);
}
&:last-child {
border-bottom: none;
}
}
.file-selected {
background-color: var(--clr-scale-pop-80);
background-color: var(--clr-theme-pop-bg);
& .files-attacment__file-name {
opacity: 0.9;

View File

@ -16,6 +16,8 @@
// Style props
export let style: ComponentColor = 'neutral';
export let kind: ComponentStyleKind = 'soft';
const SLOTS = $$props.$$slots;
</script>
<div
@ -30,9 +32,12 @@
on:mousedown
on:contextmenu
>
<span class="label">
<slot />
</span>
{#if SLOTS?.default}
<span class="label">
<slot />
</span>
{/if}
{#if loading}
<Icon name="spinner" />
{:else if icon}

View File

@ -1,35 +1,12 @@
import { formatDistanceToNowStrict } from 'date-fns';
import { writable, type Readable } from 'svelte/store';
export function createTimeAgoStore(
date: Date | undefined,
addSuffix: boolean = false
): Readable<string> | undefined {
if (!date) return;
let timeoutId: number;
return writable<string>(formatDistanceToNowStrict(date, { addSuffix }), (set) => {
function updateStore() {
if (!date) return;
const seconds = Math.round(Math.abs((new Date().getTime() - date.getTime()) / 1000.0));
const msUntilNextUpdate = Number.isNaN(seconds)
? 1000
: getSecondsUntilUpdate(seconds) * 1000;
if (seconds < 10) {
set('just now');
} else if (seconds < 60) {
set(`< 1 min ${addSuffix ? ' ago' : ''}`);
} else {
set(customFormatDistance(date, addSuffix));
}
timeoutId = window.setTimeout(() => {
updateStore();
}, msUntilNextUpdate);
}
updateStore();
return () => {
clearTimeout(timeoutId);
};
});
function customFormatDistance(date: Date, addSuffix: boolean): string {
const distance = formatDistanceToNowStrict(date, { addSuffix });
return distance.replace(
/\b(seconds?|minutes?|hours?|days?|months?|years?)\b/g,
(match) => unitShorthandMap[match]
);
}
function getSecondsUntilUpdate(seconds: number) {
@ -47,14 +24,52 @@ function getSecondsUntilUpdate(seconds: number) {
}
}
export function getTimeAgo(date: Date, addSuffix: boolean = true): string {
const seconds = Math.round(Math.abs((new Date().getTime() - date.getTime()) / 1000.0));
if (seconds < 10) {
return 'just now';
} else if (seconds < 60) {
return `< 1 min ${addSuffix ? ' ago' : ''}`;
} else {
return customFormatDistance(date, addSuffix);
}
}
export function createTimeAgoStore(
date: Date | undefined,
addSuffix: boolean = false
): Readable<string> | undefined {
if (!date) return;
let timeoutId: number;
return writable<string>(getTimeAgo(date, addSuffix), (set) => {
function updateStore() {
if (!date) return;
const seconds = Math.round(Math.abs((new Date().getTime() - date.getTime()) / 1000.0));
const msUntilNextUpdate = Number.isNaN(seconds)
? 1000
: getSecondsUntilUpdate(seconds) * 1000;
set(getTimeAgo(date, addSuffix));
timeoutId = window.setTimeout(() => {
updateStore();
}, msUntilNextUpdate);
}
updateStore();
return () => {
clearTimeout(timeoutId);
};
});
}
// SHORTHAND WORDS
const unitShorthandMap: Record<string, string> = {
second: 'sec',
seconds: 'sec',
minute: 'min',
minutes: 'min',
hour: 'hr',
hours: 'hr',
hour: 'hour',
hours: 'hours',
day: 'day',
days: 'days',
month: 'mo',
@ -62,11 +77,3 @@ const unitShorthandMap: Record<string, string> = {
year: 'yr',
years: 'yr'
};
function customFormatDistance(date: Date, addSuffix: boolean): string {
const distance = formatDistanceToNowStrict(date, { addSuffix });
return distance.replace(
/\b(seconds?|minutes?|hours?|days?|months?|years?)\b/g,
(match) => unitShorthandMap[match]
);
}

View File

@ -17,7 +17,7 @@
/* CSS VARIABLES */
:root {
--transition-fast: 0.06s ease-in-out;
--transition-medium: 0.1s ease-in-out;
--transition-medium: 0.15s ease-in-out;
--transition-slow: 0.2s ease-in-out;
/* Hover ratio for oklch */