mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 20:54:50 +03:00
Integration modal design (#5116)
* new IntegrationListItem component * Simple commit row added, componenets rename * Tweak styles and `Select` update * check fix
This commit is contained in:
parent
6b6fc9b016
commit
c7342b1cdf
@ -27,6 +27,7 @@
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
|
||||
import { getTimeAndAuthor } from '@gitbutler/ui/utils/getTimeAndAuthor';
|
||||
import { getTimeAgo } from '@gitbutler/ui/utils/timeAgo';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
@ -114,12 +115,6 @@
|
||||
commitMessageModal.close();
|
||||
}
|
||||
|
||||
function getTimeAndAuthor() {
|
||||
const timeAgo = getTimeAgo(commit.createdAt);
|
||||
const author = type === 'localAndRemote' || type === 'remote' ? commit.author.name : 'you';
|
||||
return `${timeAgo} by ${author}`;
|
||||
}
|
||||
|
||||
const commitShortSha = commit.id.substring(0, 7);
|
||||
|
||||
let topHeightPx = 24;
|
||||
@ -381,7 +376,8 @@
|
||||
</button>
|
||||
{/if}
|
||||
<span class="commit__subtitle-divider">•</span>
|
||||
<span>{getTimeAndAuthor()}</span>
|
||||
|
||||
<span>{getTimeAndAuthor(commit.createdAt, commit.author.name)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
|
||||
import CommitCard from '$lib/commit/CommitCard.svelte';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import { openExternalUrl } from '$lib/utils/url';
|
||||
import {
|
||||
getBaseBrancheResolution,
|
||||
getResolutionApproach,
|
||||
@ -16,8 +17,12 @@
|
||||
type Resolution
|
||||
} from '$lib/vbranches/upstreamIntegrationService';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Badge from '@gitbutler/ui/Badge.svelte';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import IntegrationSeriesRow from '@gitbutler/ui/IntegrationSeriesRow.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import SimpleCommitRow from '@gitbutler/ui/SimpleCommitRow.svelte';
|
||||
import { pxToRem } from '@gitbutler/ui/utils/pxToRem';
|
||||
import { tick } from 'svelte';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import type { Readable } from 'svelte/store';
|
||||
@ -40,10 +45,11 @@
|
||||
let integratingUpstream = $state<OperationState>('inert');
|
||||
let results = $state(new SvelteMap<string, Resolution>());
|
||||
let statuses = $state<BranchStatusInfo[]>([]);
|
||||
let expanded = $state<boolean>(false);
|
||||
let baseResolutionApproach = $state<BaseBranchResolutionApproach>('hardReset');
|
||||
let baseResolutionApproach = $state<BaseBranchResolutionApproach | undefined>();
|
||||
let targetCommitOid = $state<string | undefined>(undefined);
|
||||
|
||||
let isDivergedResolved = $derived($base?.diverged && !baseResolutionApproach);
|
||||
|
||||
$effect(() => {
|
||||
if ($branchStatuses?.type !== 'updatesRequired') {
|
||||
statuses = [];
|
||||
@ -82,7 +88,7 @@
|
||||
// Resolve the target commit oid if the base branch diverged and the the resolution
|
||||
// approach is changed
|
||||
$effect(() => {
|
||||
if ($base?.diverged) {
|
||||
if ($base?.diverged && baseResolutionApproach) {
|
||||
upstreamIntegrationService.resolveUpstreamIntegration(baseResolutionApproach).then((Oid) => {
|
||||
targetCommitOid = Oid;
|
||||
});
|
||||
@ -96,7 +102,10 @@
|
||||
async function integrate() {
|
||||
integratingUpstream = 'loading';
|
||||
await tick();
|
||||
const baseResolution = getBaseBrancheResolution(targetCommitOid, baseResolutionApproach);
|
||||
const baseResolution = getBaseBrancheResolution(
|
||||
targetCommitOid,
|
||||
baseResolutionApproach || 'hardReset'
|
||||
);
|
||||
await upstreamIntegrationService.integrateUpstream(
|
||||
Array.from(results.values()),
|
||||
baseResolution
|
||||
@ -109,7 +118,6 @@
|
||||
|
||||
export async function show() {
|
||||
integratingUpstream = 'inert';
|
||||
expanded = false;
|
||||
branchStatuses = upstreamIntegrationService.upstreamStatuses();
|
||||
await tick();
|
||||
modal?.show();
|
||||
@ -122,244 +130,199 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} title="Integrate upstream changes" {onClose} width="small" noPadding>
|
||||
<ScrollableContainer maxHeight="50vh">
|
||||
<div class="modal-content">
|
||||
{#if $base}
|
||||
<div class="upstream-commits">
|
||||
{#each $base.upstreamCommits.slice(0, 2) as commit, index}
|
||||
<CommitCard
|
||||
{commit}
|
||||
first={index === 0}
|
||||
last={(() => {
|
||||
if (expanded) {
|
||||
return $base.upstreamCommits.length - 1 === index;
|
||||
} else {
|
||||
if ($base.upstreamCommits.length > 2) {
|
||||
return index === 1;
|
||||
} else {
|
||||
return $base.upstreamCommits.length - 1 === index;
|
||||
<Modal bind:this={modal} {onClose} width={560} noPadding>
|
||||
<!-- <ScrollableContainer maxHeight="50vh"> -->
|
||||
<ScrollableContainer maxHeight={'70vh'}>
|
||||
{#if $base}
|
||||
<div class="section">
|
||||
<h3 class="text-14 text-semibold section-title">
|
||||
<span>Incoming changes</span><Badge label={$base.upstreamCommits.length} />
|
||||
</h3>
|
||||
<div class="scroll-wrap">
|
||||
<ScrollableContainer maxHeight={pxToRem(268)}>
|
||||
{#each $base.upstreamCommits as commit}
|
||||
<SimpleCommitRow
|
||||
title={commit.descriptionTitle}
|
||||
sha={commit.id}
|
||||
date={commit.createdAt}
|
||||
author={commit.author.name}
|
||||
onUrlOpen={() => {
|
||||
if ($gitHost) {
|
||||
openExternalUrl($gitHost.commitUrl(commit.id));
|
||||
}
|
||||
}
|
||||
})()}
|
||||
isUnapplied={true}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="remote"
|
||||
filesToggleable={false}
|
||||
/>
|
||||
{/each}
|
||||
{#if $base.upstreamCommits.length > 2}
|
||||
{#if expanded}
|
||||
{#each $base.upstreamCommits.slice(2) as commit, index}
|
||||
<CommitCard
|
||||
{commit}
|
||||
last={index === $base.upstreamCommits.length - 3}
|
||||
isUnapplied={true}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="remote"
|
||||
filesToggleable={false}
|
||||
/>
|
||||
{/each}
|
||||
<div class="commit-expand-button">
|
||||
<Button wide onclick={() => (expanded = false)}>Hide</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="commit-expand-button">
|
||||
<Button wide onclick={() => (expanded = true)}
|
||||
>Show more ({$base.upstreamCommits.length - 2})</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
}}
|
||||
onCopy={() => {
|
||||
copyToClipboard(commit.id);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</ScrollableContainer>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $base?.diverged}
|
||||
<div class="branch-status base-branch">
|
||||
<div class="description">
|
||||
<div class="description-header">
|
||||
<img class="icon" src="/images/domain-icons/trunk.svg" alt="" />
|
||||
<h5 class="text-16">{$base.branchName ?? 'Unknown'}</h5>
|
||||
</div>
|
||||
<Button
|
||||
clickable={false}
|
||||
size="tag"
|
||||
style="warning"
|
||||
outline
|
||||
reversedDirection
|
||||
shrinkable>Diverged</Button
|
||||
>
|
||||
</div>
|
||||
{#if $base?.diverged}
|
||||
<div class="target-divergence">
|
||||
<img class="target-icon" src="/images/domain-icons/trunk.svg" alt="" />
|
||||
|
||||
<div class="action">
|
||||
<Select
|
||||
value={baseResolutionApproach}
|
||||
onselect={handleBaseResolutionSelection}
|
||||
options={[
|
||||
{ label: 'Rebase', value: 'rebase' },
|
||||
{ label: 'Merge', value: 'merge' },
|
||||
{ label: 'Hard reset', value: 'hardReset' }
|
||||
]}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={highlighted} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
</div>
|
||||
<div class="target-divergence-about">
|
||||
<h3 class="text-14 text-semibold">Target branch divergence</h3>
|
||||
<p class="text-12 text-body target-divergence-description">
|
||||
Branch target/main has diverged from the workspace.
|
||||
<br />
|
||||
Select an action to proceed with updating.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if statuses.length > 0}
|
||||
<div class="statuses">
|
||||
{#each statuses as { branch, status }}
|
||||
<div class="branch-status" class:integrated={status.type === 'fullyIntegrated'}>
|
||||
<div class="description">
|
||||
<div class="description-header">
|
||||
<Button
|
||||
clickable={false}
|
||||
size="tag"
|
||||
icon="virtual-branch-small"
|
||||
style="neutral"
|
||||
reversedDirection
|
||||
/>
|
||||
<h5 class="text-16">{branch?.name || 'Unknown'}</h5>
|
||||
</div>
|
||||
{#if status.type === 'conflicted'}
|
||||
<Button clickable={false} size="tag" style="warning" outline reversedDirection
|
||||
>Conflicted</Button
|
||||
>
|
||||
{:else if status.type === 'saflyUpdatable' || status.type === 'empty'}
|
||||
<Button clickable={false} size="tag" style="neutral" outline reversedDirection
|
||||
>No conflicts</Button
|
||||
>
|
||||
{:else if status.type === 'fullyIntegrated'}
|
||||
<Button
|
||||
clickable={false}
|
||||
size="tag"
|
||||
icon="pr-small"
|
||||
style="success"
|
||||
kind="solid"
|
||||
reversedDirection>Integrated</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="action" class:action--centered={status.type === 'fullyIntegrated'}>
|
||||
{#if status.type === 'fullyIntegrated'}
|
||||
<p class="text-12 text-light info">Changes included in base branch</p>
|
||||
{:else if results.get(branch.id)}
|
||||
<Select
|
||||
value={results.get(branch.id)!.approach.type}
|
||||
onselect={(value) => {
|
||||
const result = results.get(branch.id)!;
|
||||
|
||||
results.set(branch.id, { ...result, approach: { type: value } });
|
||||
}}
|
||||
options={[
|
||||
{ label: 'Rebase', value: 'rebase' },
|
||||
{ label: 'Merge', value: 'merge' },
|
||||
{ label: 'Stash', value: 'unapply' }
|
||||
]}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={highlighted} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="target-divergence-action">
|
||||
<Select
|
||||
value={baseResolutionApproach}
|
||||
placeholder="Choose…"
|
||||
onselect={handleBaseResolutionSelection}
|
||||
options={[
|
||||
{ label: 'Rebase', value: 'rebase' },
|
||||
{ label: 'Merge', value: 'merge' },
|
||||
{ label: 'Hard reset', value: 'hardReset' }
|
||||
]}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={highlighted} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if statuses.length > 0}
|
||||
<div class="section" class:section-disabled={isDivergedResolved}>
|
||||
<h3 class="text-14 text-semibold">To be updated:</h3>
|
||||
<div class="scroll-wrap">
|
||||
<ScrollableContainer maxHeight={pxToRem(240)}>
|
||||
{#each statuses as { branch, status }}
|
||||
<IntegrationSeriesRow
|
||||
type={status.type === 'fullyIntegrated'
|
||||
? 'integrated'
|
||||
: status.type === 'conflicted'
|
||||
? 'conflicted'
|
||||
: 'clear'}
|
||||
title={branch.name}
|
||||
>
|
||||
{#snippet select()}
|
||||
{#if status.type !== 'fullyIntegrated' && results.get(branch.id)}
|
||||
<Select
|
||||
value={results.get(branch.id)!.approach.type}
|
||||
onselect={(value) => {
|
||||
const result = results.get(branch.id)!;
|
||||
|
||||
results.set(branch.id, { ...result, approach: { type: value } });
|
||||
}}
|
||||
options={[
|
||||
{ label: 'Rebase', value: 'rebase' },
|
||||
{ label: 'Merge', value: 'merge' },
|
||||
{ label: 'Stash', value: 'unapply' }
|
||||
]}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={highlighted} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</IntegrationSeriesRow>
|
||||
{/each}
|
||||
</ScrollableContainer>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</ScrollableContainer>
|
||||
|
||||
{#snippet controls()}
|
||||
<Button onclick={() => modal?.close()}>Cancel</Button>
|
||||
<Button onclick={integrate} style="pop" kind="solid" loading={integratingUpstream === 'loading'}
|
||||
>Integrate</Button
|
||||
>
|
||||
<div class="controls">
|
||||
<Button onclick={() => modal?.close()} style="ghost" outline>Cancel</Button>
|
||||
<Button
|
||||
wide
|
||||
onclick={integrate}
|
||||
style="pop"
|
||||
kind="solid"
|
||||
disabled={isDivergedResolved}
|
||||
loading={integratingUpstream === 'loading'}>Update workspace</Button
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
border-radius: var(--radius-s);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.upstream-commits {
|
||||
text-align: left;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
/* INCOMING CHANGES */
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 14px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.scroll-wrap {
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.statuses {
|
||||
box-sizing: border-box;
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* DIVERGANCE */
|
||||
.target-divergence {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
gap: 14px;
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
background-color: var(--clr-theme-warn-bg);
|
||||
}
|
||||
|
||||
.target-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.target-divergence-about {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.branch-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 14px;
|
||||
|
||||
&.base-branch {
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
}
|
||||
|
||||
&.integrated {
|
||||
background-color: var(--clr-bg-2);
|
||||
}
|
||||
|
||||
& .description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
& .action {
|
||||
width: 144px;
|
||||
|
||||
&.action--centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& .info {
|
||||
color: var(--clr-text-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.commit-expand-button {
|
||||
margin: 8px -16px;
|
||||
padding: 0 16px;
|
||||
padding-bottom: 8px;
|
||||
.target-divergence-description {
|
||||
color: var(--clr-text-2);
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
.target-divergence-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 230px;
|
||||
}
|
||||
|
||||
/* CONTROLS */
|
||||
.controls {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
.section-disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
@ -39,7 +39,7 @@
|
||||
wide,
|
||||
options = [],
|
||||
value,
|
||||
placeholder,
|
||||
placeholder = 'Select an option...',
|
||||
maxHeight,
|
||||
searchable,
|
||||
itemSnippet,
|
||||
@ -161,7 +161,7 @@
|
||||
type="select"
|
||||
reversedDirection
|
||||
icon="select-chevron"
|
||||
value={options.find((item) => item.value === value)?.label || 'Select an option...'}
|
||||
value={options.find((item) => item.value === value)?.label}
|
||||
disabled={disabled || loading}
|
||||
on:mousedown={toggleList}
|
||||
on:keydown={(ev) => handleKeyDown(ev)}
|
||||
|
139
packages/ui/src/lib/IntegrationSeriesRow.svelte
Normal file
139
packages/ui/src/lib/IntegrationSeriesRow.svelte
Normal file
@ -0,0 +1,139 @@
|
||||
<script lang="ts" module>
|
||||
import type { Snippet } from 'svelte';
|
||||
export interface Props {
|
||||
type: 'clear' | 'conflicted' | 'integrated';
|
||||
title: string;
|
||||
select: Snippet;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/Icon.svelte';
|
||||
|
||||
let { type, title, select }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="integration-series-item no-select {type}">
|
||||
<div class="branch-icon">
|
||||
<Icon name="remote-branch-small" />
|
||||
</div>
|
||||
<div class="name-label-wrap">
|
||||
<span class="name-label text-13 text-semibold truncate">
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<span class="name-label-badge text-11 text-semibold">
|
||||
{#if type === 'clear'}
|
||||
<span>Clear</span>
|
||||
{:else if type === 'conflicted'}
|
||||
<span>Conflicted</span>
|
||||
{:else if type === 'integrated'}
|
||||
<span>Integrated</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if select}
|
||||
<div class="select">
|
||||
{@render select()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if type === 'integrated'}
|
||||
<div class="integrated-label-wrap">
|
||||
<Icon name="tick-small" />
|
||||
<span class="integrated-label text-12"> Part of the new base </span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.integration-series-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 12px 12px 14px;
|
||||
min-height: 56px;
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.branch-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--clr-core-ntrl-100);
|
||||
}
|
||||
|
||||
/* NAME LABEL */
|
||||
.name-label-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.name-label {
|
||||
color: var(--clr-text-1);
|
||||
}
|
||||
|
||||
.name-label-badge {
|
||||
padding: 2px 4px;
|
||||
border-radius: var(--radius-m);
|
||||
color: var(--clr-core-ntrl-100);
|
||||
}
|
||||
|
||||
/* INTEGRATED LABEL */
|
||||
.integrated-label-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding-left: 6px;
|
||||
margin-right: 2px;
|
||||
color: var(--clr-text-2);
|
||||
}
|
||||
|
||||
.integrated-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.select {
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
&.clear {
|
||||
background-color: var(--clr-bg-1);
|
||||
|
||||
.branch-icon {
|
||||
background-color: var(--clr-core-ntrl-50);
|
||||
}
|
||||
}
|
||||
|
||||
&.conflicted {
|
||||
background-color: var(--clr-bg-1);
|
||||
|
||||
.branch-icon,
|
||||
.name-label-badge {
|
||||
background-color: var(--clr-theme-warn-on-element);
|
||||
background-color: var(--clr-theme-warn-element);
|
||||
}
|
||||
}
|
||||
|
||||
&.integrated {
|
||||
background-color: var(--clr-bg-1-muted);
|
||||
|
||||
.branch-icon,
|
||||
.name-label-badge {
|
||||
color: var(--clr-theme-purp-on-element);
|
||||
background-color: var(--clr-theme-purp-element);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/Icon.svelte';
|
||||
import { clickOutside } from '$lib/utils/clickOutside';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import type iconsJson from '$lib/data/icons.json';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
width?: 'default' | 'medium-large' | 'large' | 'small' | 'xsmall';
|
||||
width?: 'default' | 'medium-large' | 'large' | 'small' | 'xsmall' | number;
|
||||
title?: string;
|
||||
icon?: keyof typeof iconsJson;
|
||||
noPadding?: boolean;
|
||||
@ -63,6 +64,7 @@
|
||||
class:large={width === 'large'}
|
||||
class:small={width === 'small'}
|
||||
class:xsmall={width === 'xsmall'}
|
||||
style:width={typeof width === 'number' ? pxToRem(width) : undefined}
|
||||
>
|
||||
{#if open}
|
||||
<form
|
||||
@ -117,6 +119,9 @@
|
||||
background-color: var(--clr-bg-1);
|
||||
box-shadow: var(--fx-shadow-l);
|
||||
|
||||
/* fix for the native dialog "inherit" issue */
|
||||
text-align: left;
|
||||
|
||||
animation: dialog-zoom 0.25s cubic-bezier(0.34, 1.35, 0.7, 1);
|
||||
|
||||
/* MODIFIERS */
|
||||
|
114
packages/ui/src/lib/SimpleCommitRow.svelte
Normal file
114
packages/ui/src/lib/SimpleCommitRow.svelte
Normal file
@ -0,0 +1,114 @@
|
||||
<script lang="ts" module>
|
||||
// import type { Snippet } from 'svelte';
|
||||
export interface Props {
|
||||
title: string;
|
||||
sha: string;
|
||||
date: Date;
|
||||
author?: string;
|
||||
onCopy?: () => void;
|
||||
onUrlOpen?: () => void;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/Icon.svelte';
|
||||
import { getTimeAndAuthor } from '$lib/utils/getTimeAndAuthor';
|
||||
|
||||
let { title, sha, author, date, onCopy, onUrlOpen }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="simple-commit-item no-select">
|
||||
<Icon name="commit" />
|
||||
<div class="content">
|
||||
<span class="title text-13 text-semibold">
|
||||
{title}
|
||||
</span>
|
||||
<div class="details text-11">
|
||||
<button class="details-btn copy-btn" onclick={onCopy}>
|
||||
<span>{sha.substring(0, 7)}</span>
|
||||
<Icon name="copy-small" />
|
||||
</button>
|
||||
<span class="details-divider">•</span>
|
||||
<button class="details-btn link-btn" onclick={onUrlOpen}>
|
||||
<span>Open</span>
|
||||
<Icon name="open-link" />
|
||||
</button>
|
||||
|
||||
<span class="details-divider">•</span>
|
||||
<span class="truncate">{getTimeAndAuthor(date, author)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.simple-commit-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 12px 14px 14px 12px;
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
/* Fix because of using native dialog element */
|
||||
& span {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--clr-text-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.details-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
& span {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--clr-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
& span {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
& span {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-divider {
|
||||
color: var(--clr-text-3);
|
||||
line-height: 150%;
|
||||
}
|
||||
}
|
||||
</style>
|
11
packages/ui/src/lib/utils/getTimeAndAuthor.ts
Normal file
11
packages/ui/src/lib/utils/getTimeAndAuthor.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { getTimeAgo } from './timeAgo';
|
||||
|
||||
export function getTimeAndAuthor(createdAt: Date, name: string | undefined): string {
|
||||
const timeAgo = getTimeAgo(createdAt);
|
||||
|
||||
if (name) {
|
||||
return `${timeAgo} by ${name}`;
|
||||
}
|
||||
|
||||
return timeAgo;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import IntegrationSeriesListItem, {
|
||||
type Props as SeriesProps
|
||||
} from '$lib/IntegrationSeriesRow.svelte';
|
||||
|
||||
const items = [
|
||||
{ type: 'clear', title: 'Move context into shared' },
|
||||
{ type: 'conflicted', title: 'Add new context' },
|
||||
{ type: 'integrated', title: 'Make edit mode fantastic' }
|
||||
] as SeriesProps[];
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
{#each items as item}
|
||||
<IntegrationSeriesListItem {...item} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,15 @@
|
||||
import DemoIntegrationSeriesRow from './DemoIntegrationSeriesRow.svelte';
|
||||
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'List items / Integration Series Row',
|
||||
component: DemoIntegrationSeriesRow
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Story: Story = {
|
||||
name: 'Integration Series Row',
|
||||
args: {}
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import SimpleCommitRow, { type Props as SeriesProps } from '$lib/SimpleCommitRow.svelte';
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: 'feat: add user authentication',
|
||||
sha: 'b52f23',
|
||||
author: 'Jane Smith',
|
||||
date: new Date('2022-01-15T08:30:00Z')
|
||||
},
|
||||
{
|
||||
title: 'refactor: improve database queries',
|
||||
sha: 'a9f732',
|
||||
author: 'Mike Brown',
|
||||
date: new Date('2023-05-23T14:15:00Z')
|
||||
},
|
||||
{
|
||||
title: 'fix: correct null pointer exception',
|
||||
sha: 'e7c245',
|
||||
author: 'Alice White',
|
||||
date: new Date('2023-07-12T09:45:00Z')
|
||||
},
|
||||
{
|
||||
title: 'docs: update README with API examples',
|
||||
sha: '2ac923',
|
||||
author: 'Sam Green',
|
||||
date: new Date('2021-10-21T17:00:00Z')
|
||||
}
|
||||
] as SeriesProps[];
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
{#each items as item}
|
||||
<SimpleCommitRow {...item} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,15 @@
|
||||
import DemoSimpleCommitRow from './DemoSimpleCommitRow.svelte';
|
||||
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'List items / Simple Commit Row',
|
||||
component: DemoSimpleCommitRow
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Story: Story = {
|
||||
name: 'Simple Commit Row',
|
||||
args: {}
|
||||
};
|
@ -3,8 +3,8 @@ import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
|
||||
const meta = {
|
||||
title: 'Inputs / Checkbox',
|
||||
component: DemoCheckbox
|
||||
} satisfies Meta<DemoCheckbox>;
|
||||
component: DemoCheckbox as any
|
||||
} satisfies Meta<typeof DemoCheckbox>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
@ -2,7 +2,7 @@ import FileIcon from '$lib/file/FileIcon.svelte';
|
||||
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
|
||||
const meta = {
|
||||
title: 'Files / File Icon',
|
||||
title: 'Elements / File Icon',
|
||||
component: FileIcon
|
||||
} satisfies Meta<FileIcon>;
|
||||
|
||||
|
@ -2,9 +2,9 @@ import DemoFileListItem from './DemoFileListItem.svelte';
|
||||
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
|
||||
const meta = {
|
||||
title: 'Files / FileListItem',
|
||||
component: DemoFileListItem
|
||||
} satisfies Meta<DemoFileListItem>;
|
||||
title: 'List items / FileListItem',
|
||||
component: DemoFileListItem as any
|
||||
} satisfies Meta<typeof DemoFileListItem>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
@ -52,3 +52,7 @@ pre {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
user-select: none;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user