Replace repo sidebar with horizontal tab bar

Convert the vertical sidebar navigation (Source, Issues, Patches) into
a horizontal tab bar above the content area. The ContextRepo info moves
into a popover behind an info button in the tab bar.

Mobile footer navigation is preserved for small screens, with the tab
bar hidden on mobile.
This commit is contained in:
Julien Donck
2026-03-17 12:38:43 -06:00
committed by Rūdolfs Ošiņš
parent 61c4e4edd8
commit c6c3cc2807
31 changed files with 670 additions and 912 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
<Link
style="width: 100%; display: flex; align-items: center; justify-content: center;"
route={{ resource: "nodes", params: undefined }}>
<Icon name="logo" />
<Icon name="seed" />
</Link>
<slot />
+10 -22
View File
@@ -16,24 +16,20 @@
display: flex;
height: 3rem;
align-items: center;
padding: 0 0.5rem 0 1rem;
border: 1px solid var(--color-border-subtle);
border-top-left-radius: var(--border-radius-md);
border-top-right-radius: var(--border-radius-md);
padding: 0 1rem;
background-color: var(--color-surface-canvas);
z-index: 2;
}
.header-border {
border-bottom: 1px solid var(--color-border-subtle);
}
.sticky {
position: sticky;
top: 0;
}
.collapsed {
border-radius: var(--border-radius-md);
border: 1px solid var(--color-border-subtle);
}
.left {
display: flex;
gap: 0.5rem;
@@ -51,27 +47,19 @@
.container {
position: relative;
overflow-x: auto;
border: 1px solid var(--color-border-subtle);
border-top: 0;
border-bottom-left-radius: var(--border-radius-md);
border-bottom-right-radius: var(--border-radius-md);
}
@media (max-width: 719.98px) {
.header {
border-radius: 0;
border-left: 0;
border-right: 0;
padding: 0 1rem 0 1rem;
}
.container {
border-radius: 0;
border-left: 0;
border-right: 0;
}
}
</style>
<div bind:this={header} class="header" class:collapsed={!expanded} class:sticky>
<div
bind:this={header}
class="header"
class:sticky
class:header-border={expanded}>
<div class="left">
{#if collapsable}
<ExpandButton
-1
View File
@@ -4,7 +4,6 @@
<style>
.list {
border-top: 1px solid var(--color-border-subtle);
border-bottom: 1px solid var(--color-border-subtle);
}
.list-item:not(:last-child) {
+1 -1
View File
@@ -110,7 +110,7 @@
color: var(--color-text-on-brand);
}
@container (max-width: 20rem) {
@container (max-width: 30rem) {
.activity {
display: none;
}
+6 -1
View File
@@ -58,6 +58,11 @@
background-color: var(--color-surface-base);
padding: 1rem;
}
.file {
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-md);
overflow: clip;
}
.summary {
font: var(--txt-body-m-regular);
}
@@ -99,7 +104,7 @@
<Observer let:filesVisibility let:observer>
{#each diff.files as file}
{@const path = "path" in file ? file.path : file.newPath}
<div use:intersection={observer} id={"observer:" + path}>
<div class="file" use:intersection={observer} id={"observer:" + path}>
{#if "diff" in file}
<FileDiff
{repoId}
+2 -5
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { BaseUrl, Commit, Repo, SeedingPolicy } from "@http-client";
import type { BaseUrl, Commit, Repo } from "@http-client";
import dompurify from "dompurify";
import escape from "lodash/escape";
@@ -14,10 +14,8 @@
import Layout from "./Layout.svelte";
import Link from "@app/components/Link.svelte";
import Separator from "./Separator.svelte";
import Share from "./Share.svelte";
export let baseUrl: BaseUrl;
export let seedingPolicy: SeedingPolicy;
export let commit: Commit;
export let repo: Repo;
export let nodeAvatarUrl: string | undefined;
@@ -70,7 +68,7 @@
}
</style>
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {repo}>
<Layout {nodeAvatarUrl} {baseUrl} {repo}>
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
@@ -117,7 +115,6 @@
</Button>
</a>
{/if}
<Share />
</div>
</span>
<CommitAuthorship {header}>
+74 -22
View File
@@ -5,20 +5,65 @@
<script lang="ts">
import type { BaseUrl, Repo } from "@http-client";
import Link from "@app/components/Link.svelte";
import config from "@app/lib/config";
import debounce from "lodash/debounce";
import { routeToPath } from "@app/lib/router";
import { toClipboard } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
import Icon from "@app/components/Icon.svelte";
import Link from "@app/components/Link.svelte";
import SeedButton from "@app/views/repos/Header/SeedButton.svelte";
export let baseUrl: BaseUrl;
export let activeTab: ActiveTab = undefined;
export let repo: Repo;
let shareIcon: "link" | "checkmark" = "link";
const restoreIcon = debounce(() => {
shareIcon = "link";
}, 1000);
function tabRoute(): string {
if (activeTab === "issues") {
return routeToPath({
resource: "repo.issues",
repo: repo.rid,
node: baseUrl,
});
} else if (activeTab === "patches") {
return routeToPath({
resource: "repo.patches",
repo: repo.rid,
node: baseUrl,
});
} else {
return routeToPath({
resource: "repo.source",
repo: repo.rid,
node: baseUrl,
path: "/",
});
}
}
async function copyLink() {
const origin = new URL(config.nodes.fallbackPublicExplorer).origin;
await toClipboard(origin.concat(tabRoute()));
shareIcon = "checkmark";
restoreIcon();
}
</script>
<style>
.container {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex-direction: row;
align-items: center;
gap: 0.25rem;
padding: 1rem;
border-bottom: 1px solid var(--color-border-subtle);
}
.counter {
@@ -41,8 +86,16 @@
.title-counter {
display: flex;
gap: 0.5rem;
justify-content: space-between;
width: 100%;
}
.spacer {
flex: 1;
}
.actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>
@@ -54,11 +107,7 @@
node: baseUrl,
path: "/",
}}>
<Button
size="large"
styleWidth="100%"
styleJustifyContent="flex-start"
variant={activeTab === "source" ? "gray" : "background"}>
<Button variant={activeTab === "source" ? "gray" : "background"}>
<Icon name="chevron-left-right" />
Source
</Button>
@@ -69,12 +118,7 @@
repo: repo.rid,
node: baseUrl,
}}>
<Button
let:hover
size="large"
styleJustifyContent="flex-start"
styleWidth="100%"
variant={activeTab === "issues" ? "gray" : "background"}>
<Button let:hover variant={activeTab === "issues" ? "gray" : "background"}>
<Icon name="issue" />
<div class="title-counter">
Issues
@@ -94,12 +138,7 @@
repo: repo.rid,
node: baseUrl,
}}>
<Button
let:hover
size="large"
styleWidth="100%"
styleJustifyContent="flex-start"
variant={activeTab === "patches" ? "gray" : "background"}>
<Button let:hover variant={activeTab === "patches" ? "gray" : "background"}>
<Icon name="patch" />
<div class="title-counter">
Patches
@@ -112,4 +151,17 @@
</div>
</Button>
</Link>
<div class="spacer"></div>
<div class="actions">
{#if activeTab !== "issues" && activeTab !== "patches"}
<Button variant="outline" size="regular" on:click={copyLink}>
<Icon name={shareIcon} />
<span class="global-hide-on-small-desktop-down">Copy link</span>
</Button>
{/if}
<slot name="actions" />
<SeedButton seedCount={repo.seeding} repoId={repo.rid} />
</div>
</div>
+13 -1
View File
@@ -11,11 +11,23 @@
import Popover from "@app/components/Popover.svelte";
import Radio from "@app/components/Radio.svelte";
import { baseUrlToString } from "@app/lib/utils";
export let baseUrl: BaseUrl;
export let id: string;
export let name: string;
export let currentRefname: string;
export let enabledArchiveDownload: boolean;
let enabledArchiveDownload = false;
void fetch(
`${baseUrlToString(baseUrl)}/raw/${id}/archive/${currentRefname}`,
{
method: "HEAD",
},
).then(response => {
enabledArchiveDownload = response.ok;
});
let activeTab: "radicle" | "git" | "archive" = "radicle";
+3 -3
View File
@@ -27,8 +27,8 @@
padding: 0 0.25rem;
}
.not-seeding {
background-color: var(--color-surface-brand-secondary);
color: var(--color-text-on-brand);
background-color: var(--color-surface-mid);
color: var(--color-text-secondary);
}
.disabled {
background-color: var(--color-surface-mid);
@@ -44,7 +44,7 @@
on:click={() => {
toggle();
}}
variant="secondary-toggle-off">
variant="gray">
<Icon name="seed" />
<span class="title-counter">
<span class="global-hide-on-mobile-down">Seed</span>
+39 -43
View File
@@ -15,6 +15,7 @@
import { groupCommits } from "@app/lib/commit";
import Button from "@app/components/Button.svelte";
import CloneButton from "@app/views/repos/Header/CloneButton.svelte";
import CommitTeaser from "./Commit/CommitTeaser.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
import Header from "./Source/Header.svelte";
@@ -36,7 +37,21 @@
export let tree: Tree;
export let nodeAvatarUrl: string | undefined;
$: currentRefname = formatQualifiedRefname(
revision || repo.payloads["xyz.radicle.project"].data.defaultBranch,
peer,
);
const api = new HttpdClient(baseUrl);
let totalCommits: number | undefined = undefined;
function fetchTotalCommits(rid: string, sha: string) {
void api.repo.getTreeStatsBySha(rid, sha).then(stats => {
totalCommits = stats.commits;
});
}
$: fetchTotalCommits(repo.rid, commit);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let error: any;
@@ -80,18 +95,13 @@
justify-content: center;
}
.group-header {
margin-left: 1rem;
margin-top: 3rem;
margin-bottom: 1rem;
margin: 1rem 0 0.5rem 1rem;
font: var(--txt-body-m-regular);
color: var(--color-text-tertiary);
}
.group-header:first-child {
margin-top: 0;
}
</style>
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {repo} activeTab="source">
<Layout {nodeAvatarUrl} {baseUrl} {repo} activeTab="source">
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
@@ -103,16 +113,19 @@
Commits
</Link>
</svelte:fragment>
<RepoNameHeader
{repo}
currentRefname={formatQualifiedRefname(
revision || repo.payloads["xyz.radicle.project"].data.defaultBranch,
peer,
)}
{baseUrl}
slot="header" />
<svelte:fragment slot="actions">
<CloneButton
{baseUrl}
{currentRefname}
id={repo.rid}
name={repo.payloads["xyz.radicle.project"].data.name} />
</svelte:fragment>
<RepoNameHeader {repo} {baseUrl} {seedingPolicy} slot="header" />
<div style:margin="1rem" slot="subheader">
<div
style:padding="1rem"
style:border-bottom="1px solid var(--color-border-subtle)"
slot="subheader">
<Header
{baseRoute}
{commit}
@@ -140,41 +153,24 @@
{/each}
</div>
{#await api.repo.getTreeStatsBySha(repo.rid, commit)}
{#if totalCommits === undefined || loading || allCommitHeaders.length < totalCommits}
<div class="more">
<Loading small center />
{#if totalCommits === undefined || loading}
<Loading small={page !== 0} center />
{:else if allCommitHeaders.length < totalCommits}
<Button size="large" variant="outline" on:click={loadMore}>More</Button>
{/if}
</div>
{:then stats}
{#if loading || allCommitHeaders.length < stats.commits}
<div class="more">
{#if loading}
<Loading small={page !== 0} center />
{:else if allCommitHeaders.length < stats.commits}
<Button size="large" variant="outline" on:click={loadMore}>
More
</Button>
{/if}
</div>
{/if}
{/if}
{#if error}
<div class="message">
<ErrorMessage
title="Couldn't load commits"
description="Make sure you are able to connect to the seed <code>{baseUrlToString(
api.baseUrl,
)}</code>"
{error} />
</div>
{/if}
{:catch error}
{#if error}
<div class="message">
<ErrorMessage
title="Couldn't load repo stats"
title="Couldn't load commits"
description="Make sure you are able to connect to the seed <code>{baseUrlToString(
api.baseUrl,
)}</code>"
{error} />
</div>
{/await}
{/if}
</Layout>
+1 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { BaseUrl, Issue, Repo, SeedingPolicy } from "@http-client";
import type { BaseUrl, Issue, Repo } from "@http-client";
import capitalize from "lodash/capitalize";
import uniqBy from "lodash/uniqBy";
@@ -24,7 +24,6 @@
import ThreadComponent from "@app/components/Thread.svelte";
export let baseUrl: BaseUrl;
export let seedingPolicy: SeedingPolicy;
export let issue: Issue;
export let repo: Repo;
export let rawPath: (commit?: string) => string;
@@ -122,7 +121,6 @@
{baseUrl}
{nodeAvatarUrl}
{repo}
{seedingPolicy}
activeTab="issues"
stylePaddingBottom="0">
<svelte:fragment slot="breadcrumb">
+45 -92
View File
@@ -1,21 +1,11 @@
<script lang="ts">
import type {
BaseUrl,
Issue,
IssueState,
Repo,
SeedingPolicy,
} from "@http-client";
import type { BaseUrl, Issue, IssueState, Repo } from "@http-client";
import capitalize from "lodash/capitalize";
import { HttpdClient } from "@http-client";
import { ISSUES_PER_PAGE } from "./router";
import { baseUrlToString } from "@app/lib/utils";
import { closeFocused } from "@app/components/Popover.svelte";
import Button from "@app/components/Button.svelte";
import DropdownList from "@app/components/DropdownList.svelte";
import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
import Icon from "@app/components/Icon.svelte";
import IssueTeaser from "@app/views/repos/Issue/IssueTeaser.svelte";
@@ -24,12 +14,9 @@
import List from "@app/components/List.svelte";
import Loading from "@app/components/Loading.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
import Popover from "@app/components/Popover.svelte";
import Separator from "./Separator.svelte";
import Share from "./Share.svelte";
export let baseUrl: BaseUrl;
export let seedingPolicy: SeedingPolicy;
export let issues: Issue[];
export let repo: Repo;
export let status: IssueState["status"];
@@ -65,17 +52,6 @@
}
}
const stateOptions: IssueState["status"][] = ["open", "closed"];
const stateColor: Record<IssueState["status"], string> = {
open: "var(--color-text-open)",
closed: "var(--color-text-merged)",
};
const stateBackground: Record<IssueState["status"], string> = {
open: "var(--color-surface-open)",
closed: "var(--color-surface-merged)",
};
$: showMoreButton =
!loading &&
!error &&
@@ -85,8 +61,9 @@
<style>
.header {
display: flex;
justify-content: space-between;
gap: 0.25rem;
padding: 1rem;
border-bottom: 1px solid var(--color-border-subtle);
}
.more {
margin-top: 2rem;
@@ -95,21 +72,22 @@
align-items: center;
justify-content: center;
}
.dropdown-button-counter {
border-radius: var(--border-radius-sm);
background-color: var(--color-surface-alpha-subtle);
color: var(--color-text-primary);
padding: 0 0.25rem;
}
.dropdown-list-counter {
.counter {
border-radius: var(--border-radius-sm);
background-color: var(--color-surface-mid);
color: var(--color-text-tertiary);
padding: 0 0.25rem;
min-width: 1.5rem;
text-align: center;
}
.selected {
background-color: var(--color-surface-alpha-subtle);
color: var(--color-text-tertiary);
color: var(--color-text-primary);
}
.title-counter {
display: flex;
align-items: center;
gap: 0.5rem;
}
.placeholder {
height: calc(100% - 4rem);
@@ -124,7 +102,7 @@
}
</style>
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {repo} activeTab="issues">
<Layout {nodeAvatarUrl} {baseUrl} {repo} activeTab="issues">
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
@@ -137,65 +115,40 @@
</Link>
</svelte:fragment>
<div slot="header" class="header">
<Popover
popoverPadding="0"
popoverPositionTop="2.5rem"
popoverBorderRadius="var(--border-radius-md)">
<Button
let:expanded
slot="toggle"
let:toggle
on:click={toggle}
ariaLabel="filter-dropdown"
title="Filter issues by state">
<div
style:color={stateColor[status]}
style:background={stateBackground[status]}
style:padding="0.25rem 0.25rem"
style:border-radius="var(--border-radius-sm)">
<Icon name={status === "closed" ? "issue-closed" : "issue"} />
<Link
route={{
resource: "repo.issues",
repo: repo.rid,
node: baseUrl,
status: "open",
}}>
<Button variant={status === "open" ? "gray" : "background"}>
<Icon name="issue" />
<div class="title-counter">
Open
<span class="counter" class:selected={status === "open"}>
{repo.payloads["xyz.radicle.project"].meta.issues.open}
</span>
</div>
{capitalize(status)}
<div class="dropdown-button-counter">
{repo.payloads["xyz.radicle.project"].meta.issues[status]}
</div>
<Icon name={expanded ? "chevron-up" : "chevron-down"} />
</Button>
<DropdownList slot="popover" items={stateOptions}>
<Link
on:afterNavigate={() => closeFocused()}
slot="item"
let:item
route={{
resource: "repo.issues",
repo: repo.rid,
node: baseUrl,
status: item,
}}>
<DropdownListItem selected={item === status}>
<div
style:color={stateColor[item]}
style:background={stateBackground[item]}
style:padding="0.25rem 0.25rem"
style:border-radius="var(--border-radius-sm)">
<Icon name={item === "closed" ? "issue-closed" : "issue"} />
</div>
<div
style="display: flex; gap: 1rem;justify-content: space-between; width: 100%;">
{capitalize(item)}
<div
class="dropdown-list-counter"
class:selected={item === status}>
{repo.payloads["xyz.radicle.project"].meta.issues[item]}
</div>
</div>
</DropdownListItem>
</Link>
</DropdownList>
</Popover>
<Share />
</Link>
<Link
route={{
resource: "repo.issues",
repo: repo.rid,
node: baseUrl,
status: "closed",
}}>
<Button variant={status === "closed" ? "gray" : "background"}>
<Icon name="issue-closed" />
<div class="title-counter">
Closed
<span class="counter" class:selected={status === "closed"}>
{repo.payloads["xyz.radicle.project"].meta.issues.closed}
</span>
</div>
</Button>
</Link>
</div>
<List items={allIssues}>
+18 -22
View File
@@ -1,18 +1,17 @@
<script lang="ts">
import type { ActiveTab } from "./Header.svelte";
import type { BaseUrl, Repo, SeedingPolicy } from "@http-client";
import type { BaseUrl, Repo } from "@http-client";
import Button from "@app/components/Button.svelte";
import Header from "@app/components/Header.svelte";
import Icon from "@app/components/Icon.svelte";
import Link from "@app/components/Link.svelte";
import MobileFooter from "@app/App/MobileFooter.svelte";
import RepoHeader from "./Header.svelte";
import Separator from "./Separator.svelte";
import Sidebar from "@app/views/repos/Sidebar.svelte";
import UserAvatar from "@app/components/UserAvatar.svelte";
export let activeTab: ActiveTab | undefined = undefined;
export let seedingPolicy: SeedingPolicy;
export let baseUrl: BaseUrl;
export let repo: Repo;
export let stylePaddingBottom: string = "2.5rem";
@@ -21,23 +20,18 @@
<style>
.layout {
display: grid;
grid-template: auto 1fr auto / auto 1fr auto;
display: flex;
flex-direction: column;
height: 100%;
}
.desktop-header {
grid-column: 1 / 4;
}
.sidebar {
grid-column: 1 / 2;
border-right: 1px solid var(--color-border-subtle);
}
.content {
grid-column: 2 / 3;
overflow: scroll;
flex: 1;
}
.tab-bar {
display: block;
}
.mobile-footer {
@@ -68,6 +62,9 @@
.desktop-header {
display: none;
}
.tab-bar {
display: none;
}
.content {
overflow-y: scroll;
overflow-x: hidden;
@@ -75,7 +72,6 @@
.mobile-footer {
margin-top: auto;
display: grid;
grid-column: 1 / 4;
}
}
</style>
@@ -133,12 +129,12 @@
</Header>
</div>
<div class="sidebar global-hide-on-medium-desktop-down">
<Sidebar {seedingPolicy} {activeTab} {baseUrl} {repo} />
</div>
<div class="sidebar global-hide-on-mobile-down global-hide-on-desktop-up">
<Sidebar {seedingPolicy} {activeTab} {baseUrl} {repo} collapsedOnly />
<div class="tab-bar">
<RepoHeader {activeTab} {baseUrl} {repo}>
<svelte:fragment slot="actions">
<slot name="actions" />
</svelte:fragment>
</RepoHeader>
</div>
<div class="content" style:padding-bottom={stylePaddingBottom}>
+23 -41
View File
@@ -7,7 +7,6 @@
Repo,
Revision,
Diff,
SeedingPolicy,
} from "@http-client";
interface Thread {
@@ -71,7 +70,7 @@
import Markdown from "@app/components/Markdown.svelte";
import NodeId from "@app/components/NodeId.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
import Radio from "@app/components/Radio.svelte";
import Reactions from "@app/components/Reactions.svelte";
import Reviews from "@app/views/repos/Cob/Reviews.svelte";
import RevisionComponent from "@app/views/repos/Cob/Revision.svelte";
@@ -80,7 +79,6 @@
import Share from "@app/views/repos/Share.svelte";
export let baseUrl: BaseUrl;
export let seedingPolicy: SeedingPolicy;
export let patch: Patch;
export let stats: Diff["stats"];
export let rawPath: (commit?: string) => string;
@@ -267,18 +265,14 @@
height: 100%;
}
.tabs {
font: var(--txt-body-s-regular);
display: flex;
align-items: center;
justify-content: left;
gap: 0.25rem;
flex-wrap: wrap;
position: relative;
margin-top: 1rem;
box-shadow: inset 0 -1px 0 var(--color-border-subtle);
}
.tabs-spacer {
width: 1rem;
height: 100%;
padding: 1rem;
border-top: 1px solid var(--color-border-subtle);
border-bottom: 1px solid var(--color-border-subtle);
background-color: var(--color-surface-base);
}
.author-metadata {
color: var(--color-text-tertiary);
@@ -304,7 +298,6 @@
{baseUrl}
{repo}
{nodeAvatarUrl}
{seedingPolicy}
activeTab="patches"
stylePaddingBottom="0">
<svelte:fragment slot="breadcrumb">
@@ -419,42 +412,31 @@
</CobHeader>
<div class="tabs">
<div class="tabs-spacer"></div>
<Radio styleGap="0.375rem">
{#each Object.entries(tabs) as [name, { route, icon }]}
<Link {route}>
<Button
size="large"
variant={name === view.name ||
(view.name === "diff" && name === "changes")
? "tab-active"
: "tab"}>
<Icon name={icon} />
{capitalize(name)}
</Button>
</Link>
{/each}
</Radio>
{#each Object.entries(tabs) as [name, { route, icon }]}
<Link {route}>
<Button
variant={name === view.name ||
(view.name === "diff" && name === "changes")
? "gray"
: "background"}>
<Icon name={icon} />
{capitalize(name)}
</Button>
</Link>
{/each}
{#if view.name === "changes"}
<div
class="global-hide-on-mobile-down"
style="margin-left: auto; margin-top: -0.5rem;">
<div class="global-hide-on-mobile-down" style="margin-left: auto;">
<RevisionSelector {view} {baseUrl} {patch} {repo} />
</div>
{/if}
{#if view.name === "diff"}
<div
class="global-hide-on-mobile-down"
style="margin-left: auto; margin-top: -0.5rem;">
<div style:margin-left="auto">
<CompareButton
fromCommit={view.fromCommit}
toCommit={view.toCommit} />
</div>
<div class="global-hide-on-mobile-down" style="margin-left: auto;">
<CompareButton
fromCommit={view.fromCommit}
toCommit={view.toCommit} />
</div>
{/if}
<div class="tabs-spacer"></div>
</div>
<div class="bottom">
{#if view.name === "changes"}
+82 -114
View File
@@ -1,21 +1,12 @@
<script lang="ts">
import type {
BaseUrl,
Patch,
PatchState,
Repo,
SeedingPolicy,
} from "@http-client";
import type { BaseUrl, Patch, PatchState, Repo } from "@http-client";
import { HttpdClient } from "@http-client";
import capitalize from "lodash/capitalize";
import { PATCHES_PER_PAGE } from "./router";
import { baseUrlToString } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
import DropdownList from "@app/components/DropdownList.svelte";
import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
import Icon from "@app/components/Icon.svelte";
import Layout from "./Layout.svelte";
@@ -24,12 +15,9 @@
import Loading from "@app/components/Loading.svelte";
import PatchTeaser from "./Patch/PatchTeaser.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
import Popover, { closeFocused } from "@app/components/Popover.svelte";
import Separator from "./Separator.svelte";
import Share from "./Share.svelte";
export let baseUrl: BaseUrl;
export let seedingPolicy: SeedingPolicy;
export let patches: Patch[];
export let repo: Repo;
export let status: PatchState["status"];
@@ -65,27 +53,6 @@
}
}
const stateOptions: PatchState["status"][] = [
"draft",
"open",
"archived",
"merged",
];
const stateColor: Record<PatchState["status"], string> = {
draft: "var(--color-text-draft)",
open: "var(--color-text-open)",
archived: "var(--color-text-archived)",
merged: "var(--color-text-merged)",
};
const stateBackground: Record<PatchState["status"], string> = {
draft: "var(--color-surface-draft)",
open: "var(--color-surface-open)",
archived: "var(--color-surface-archived)",
merged: "var(--color-surface-merged)",
};
$: showMoreButton =
!loading &&
!error &&
@@ -96,8 +63,9 @@
<style>
.header {
display: flex;
justify-content: space-between;
gap: 0.25rem;
padding: 1rem;
border-bottom: 1px solid var(--color-border-subtle);
}
.more {
margin-top: 2rem;
@@ -106,21 +74,22 @@
align-items: center;
justify-content: center;
}
.dropdown-button-counter {
border-radius: var(--border-radius-sm);
background-color: var(--color-surface-alpha-subtle);
color: var(--color-text-primary);
padding: 0 0.25rem;
}
.dropdown-list-counter {
.counter {
border-radius: var(--border-radius-sm);
background-color: var(--color-surface-mid);
color: var(--color-text-tertiary);
padding: 0 0.25rem;
min-width: 1.5rem;
text-align: center;
}
.selected {
background-color: var(--color-surface-alpha-subtle);
color: var(--color-text-tertiary);
color: var(--color-text-primary);
}
.title-counter {
display: flex;
align-items: center;
gap: 0.5rem;
}
.placeholder {
height: calc(100% - 4rem);
@@ -132,10 +101,13 @@
.placeholder {
height: calc(100vh - 10rem);
}
.title-counter:not(.active) {
display: none;
}
}
</style>
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {repo} activeTab="patches">
<Layout {nodeAvatarUrl} {baseUrl} {repo} activeTab="patches">
<svelte:fragment slot="breadcrumb">
<Separator />
<Link
@@ -148,78 +120,74 @@
</Link>
</svelte:fragment>
<div slot="header" class="header">
<Popover
popoverPadding="0"
popoverPositionTop="2.5rem"
popoverBorderRadius="var(--border-radius-md)">
<Button
let:expanded
slot="toggle"
let:toggle
on:click={toggle}
ariaLabel="filter-dropdown"
title="Filter patches by state">
<div
style:color={stateColor[status]}
style:background={stateBackground[status]}
style:padding="0.25rem 0.25rem"
style:border-radius="var(--border-radius-sm)">
<Icon
name={status === "draft"
? "patch-draft"
: status === "merged"
? "patch-merged"
: status === "archived"
? "patch-archived"
: "patch"} />
<Link
route={{
resource: "repo.patches",
repo: repo.rid,
node: baseUrl,
search: "status=open",
}}>
<Button variant={status === "open" ? "gray" : "background"}>
<Icon name="patch" />
<div class="title-counter" class:active={status === "open"}>
Open
<span class="counter" class:selected={status === "open"}>
{repo.payloads["xyz.radicle.project"].meta.patches.open}
</span>
</div>
{capitalize(status)}
<div class="dropdown-button-counter">
{repo.payloads["xyz.radicle.project"].meta.patches[status]}
</div>
<Icon name={expanded ? "chevron-up" : "chevron-down"} />
</Button>
<DropdownList slot="popover" items={stateOptions}>
<Link
slot="item"
let:item
on:afterNavigate={() => closeFocused()}
route={{
resource: "repo.patches",
repo: repo.rid,
node: baseUrl,
search: `status=${item}`,
}}>
<DropdownListItem selected={item === status}>
<div
style:color={stateColor[item]}
style:background={stateBackground[item]}
style:padding="0.25rem 0.25rem"
style:border-radius="var(--border-radius-sm)">
<Icon
name={item === "draft"
? "patch-draft"
: item === "merged"
? "patch-merged"
: item === "archived"
? "patch-archived"
: "patch"} />
</div>
<div
style="display: flex; gap: 1rem;justify-content: space-between; width: 100%;">
{capitalize(item)}
<div
class="dropdown-list-counter"
class:selected={item === status}>
{repo.payloads["xyz.radicle.project"].meta.patches[item]}
</div>
</div>
</DropdownListItem>
</Link>
</DropdownList>
</Popover>
<Share />
</Link>
<Link
route={{
resource: "repo.patches",
repo: repo.rid,
node: baseUrl,
search: "status=draft",
}}>
<Button variant={status === "draft" ? "gray" : "background"}>
<Icon name="patch-draft" />
<div class="title-counter" class:active={status === "draft"}>
Draft
<span class="counter" class:selected={status === "draft"}>
{repo.payloads["xyz.radicle.project"].meta.patches.draft}
</span>
</div>
</Button>
</Link>
<Link
route={{
resource: "repo.patches",
repo: repo.rid,
node: baseUrl,
search: "status=archived",
}}>
<Button variant={status === "archived" ? "gray" : "background"}>
<Icon name="patch-archived" />
<div class="title-counter" class:active={status === "archived"}>
Archived
<span class="counter" class:selected={status === "archived"}>
{repo.payloads["xyz.radicle.project"].meta.patches.archived}
</span>
</div>
</Button>
</Link>
<Link
route={{
resource: "repo.patches",
repo: repo.rid,
node: baseUrl,
search: "status=merged",
}}>
<Button variant={status === "merged" ? "gray" : "background"}>
<Icon name="patch-merged" />
<div class="title-counter" class:active={status === "merged"}>
Merged
<span class="counter" class:selected={status === "merged"}>
{repo.payloads["xyz.radicle.project"].meta.patches.merged}
</span>
</div>
</Button>
</Link>
</div>
<List items={allPatches}>
-267
View File
@@ -1,267 +0,0 @@
<script lang="ts">
import type { ActiveTab } from "./Header.svelte";
import type { BaseUrl, Repo, SeedingPolicy } from "@http-client";
import Button from "@app/components/Button.svelte";
import ContextRepo from "@app/views/repos/Sidebar/ContextRepo.svelte";
import Icon from "@app/components/Icon.svelte";
import Link from "@app/components/Link.svelte";
import Popover from "@app/components/Popover.svelte";
const SIDEBAR_STATE_KEY = "sidebarState";
export let activeTab: ActiveTab | undefined = undefined;
export let seedingPolicy: SeedingPolicy;
export let baseUrl: BaseUrl;
export let repo: Repo;
export let collapsedOnly = false;
let expanded = collapsedOnly ? false : loadSidebarState();
export function storeSidebarState(expanded: boolean): void {
if (localStorage) {
localStorage.setItem(
SIDEBAR_STATE_KEY,
expanded ? "expanded" : "collapsed",
);
} else {
console.warn(
"localStorage isn't available, not able to persist the sidebar state without it.",
);
}
}
function loadSidebarState(): boolean {
const storedSidebarState = localStorage
? localStorage.getItem(SIDEBAR_STATE_KEY)
: null;
if (storedSidebarState === null) {
return true;
} else {
return storedSidebarState === "expanded" ? true : false;
}
}
function toggleSidebar() {
expanded = !expanded;
storeSidebarState(expanded);
}
</script>
<style>
.sidebar {
padding: 1rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: width 150ms ease-in-out;
width: 4.5rem;
}
.sidebar.expanded {
width: 22.5rem;
}
.repo-navigation {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
}
.counter {
border-radius: var(--border-radius-sm);
background-color: var(--color-surface-mid);
color: var(--color-text-tertiary);
padding: 0 0.25rem;
}
.selected {
background-color: var(--color-surface-alpha-subtle);
color: var(--color-text-primary);
}
.hover {
background-color: var(--color-surface-strong);
color: var(--color-text-primary);
}
.title-counter {
display: flex;
overflow: hidden;
gap: 0.5rem;
justify-content: space-between;
width: 100%;
opacity: 0;
transition: opacity 150ms ease-in-out;
}
.title-counter.expanded {
opacity: 1;
}
.sidebar-footer {
display: flex;
justify-content: space-between;
width: 100%;
}
.repo {
z-index: 10;
opacity: 0;
height: 0;
overflow: hidden;
}
.box {
padding: 1rem;
margin-bottom: 0.5rem;
background-color: var(--color-surface-subtle);
border: 1px solid var(--color-border-subtle);
font: var(--txt-body-m-regular);
border-radius: var(--border-radius-md);
}
.repo.expanded {
opacity: 1;
height: initial;
overflow: initial;
transition: opacity 150ms;
transition-delay: 150ms;
}
.vertical-buttons {
opacity: 1;
height: fit-content;
display: flex;
flex-direction: column-reverse;
transition: opacity 150ms ease-in-out;
transition-delay: 60ms;
margin-bottom: 0.5rem;
}
.vertical-buttons.expanded {
opacity: 0;
height: 0;
overflow: hidden;
}
.icon {
transform: rotate(180deg);
transition: transform 150ms ease-in-out;
}
.icon.expanded {
transform: rotate(0deg);
}
.bottom {
display: flex;
flex-direction: column;
}
</style>
<div class="sidebar" class:expanded>
<!-- Top Navigation Items -->
<div class="repo-navigation">
<Link
title="Source"
route={{
resource: "repo.source",
repo: repo.rid,
node: baseUrl,
path: "/",
}}>
<Button
stylePadding="0.5rem 0.75rem"
size="large"
styleWidth="100%"
styleJustifyContent="flex-start"
variant={activeTab === "source" ? "gray" : "background"}>
<Icon name="chevron-left-right" />
<span class="title-counter" class:expanded>Source</span>
</Button>
</Link>
<Link
title={`${repo.payloads["xyz.radicle.project"].meta.issues.open} Issues`}
route={{
resource: "repo.issues",
repo: repo.rid,
node: baseUrl,
}}>
<Button
stylePadding="0.5rem 0.75rem"
let:hover
size="large"
styleJustifyContent="flex-start"
styleWidth="100%"
variant={activeTab === "issues" ? "gray" : "background"}>
<Icon name="issue" />
<div class="title-counter" class:expanded>
Issues
<span
class="counter"
class:selected={activeTab === "issues"}
class:hover={hover && activeTab !== "issues"}>
{repo.payloads["xyz.radicle.project"].meta.issues.open}
</span>
</div>
</Button>
</Link>
<Link
title={`${repo.payloads["xyz.radicle.project"].meta.patches.open} Patches`}
route={{
resource: "repo.patches",
repo: repo.rid,
node: baseUrl,
}}>
<Button
stylePadding="0.5rem 0.75rem"
let:hover
size="large"
styleWidth="100%"
styleJustifyContent="flex-start"
variant={activeTab === "patches" ? "gray" : "background"}>
<Icon name="patch" />
<div class="title-counter" class:expanded>
Patches
<span
class="counter"
class:hover={hover && activeTab !== "patches"}
class:selected={activeTab === "patches"}>
{repo.payloads["xyz.radicle.project"].meta.patches.open}
</span>
</div>
</Button>
</Link>
</div>
<!-- Context and other information section -->
<div class="bottom">
<div class="repo box" class:expanded>
<ContextRepo
{baseUrl}
repoThreshold={repo.threshold}
repoDelegates={repo.delegates}
{seedingPolicy} />
</div>
<div class="vertical-buttons" class:expanded style:gap="0.5rem">
<Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
<Button
stylePadding="0 0.75rem"
variant="background"
title="Info"
slot="toggle"
let:toggle
on:click={toggle}>
<Icon name="guide" />
</Button>
<div slot="popover" class="txt-body-m-regular" style:width="18rem">
<ContextRepo
{baseUrl}
repoThreshold={repo.threshold}
repoDelegates={repo.delegates}
{seedingPolicy} />
</div>
</Popover>
</div>
<!-- Footer -->
{#if !collapsedOnly}
<div class="sidebar-footer" style:flex-direction="row">
<Button title="Collapse" on:click={toggleSidebar} variant="background">
<div class="icon" class:expanded>
<Icon name="chevron-left" />
</div>
</Button>
</div>
{/if}
</div>
</div>
+57 -46
View File
@@ -2,53 +2,78 @@
import type { BaseUrl, Repo, SeedingPolicy } from "@http-client";
import capitalize from "lodash/capitalize";
import IconButton from "@app/components/IconButton.svelte";
import Icon from "@app/components/Icon.svelte";
import HoverPopover from "@app/components/HoverPopover.svelte";
import Link from "@app/components/Link.svelte";
import NodeId from "@app/components/NodeId.svelte";
import UserAvatar from "@app/components/UserAvatar.svelte";
export let baseUrl: BaseUrl;
export let repoThreshold: number;
export let repoDelegates: Repo["delegates"];
export let seedingPolicy: SeedingPolicy;
let delegateExpanded = false;
let policyExpanded = false;
</script>
<style>
.item-header {
gap: 2rem;
.context-repo {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0.2rem 0;
gap: 0.5rem;
}
.item-header:first-child {
margin-top: 0;
.label {
color: var(--color-text-tertiary);
}
.item-header:last-child {
margin-bottom: 0;
.value {
color: var(--color-text-primary);
}
.nid {
height: 21.5px;
margin: 0.5rem 0;
.avatars {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.25rem;
}
.avatars :global(.popover) {
padding: 0.25rem 0.5rem;
}
.avatar-popover {
white-space: nowrap;
}
.description {
font: var(--txt-body-s-medium);
color: var(--color-text-quaternary);
}
</style>
<div class="item-header">
<span>Delegates</span>
<div class="global-flex-item">
<span class="txt-body-m-semibold">
<div class="context-repo">
<div class="row">
<span class="label txt-body-m-medium">Delegates</span>
<span class="value txt-body-m-medium">
{repoThreshold}/{repoDelegates.length}
</span>
<IconButton on:click={() => (delegateExpanded = !delegateExpanded)}>
<Icon name={delegateExpanded ? "chevron-up" : "chevron-down"} />
</IconButton>
<div class="avatars">
{#each repoDelegates as delegate}
<HoverPopover>
<Link
slot="toggle"
style="display: flex"
route={{ resource: "users", did: delegate.id, baseUrl }}>
<UserAvatar nodeId={delegate.id} styleWidth="1rem" />
</Link>
<div slot="popover" class="avatar-popover">
<NodeId {baseUrl} nodeId={delegate.id} alias={delegate.alias} />
</div>
</HoverPopover>
{/each}
</div>
</div>
</div>
{#if delegateExpanded}
<div style:color="var(--color-text-tertiary)" style:margin-bottom="1rem">
<div class="description">
{#if repoDelegates.length === 1}
Any changes accepted by the sole delegate will be included in the
canonical branch.
@@ -57,29 +82,15 @@
to be included in the canonical branch.
{/if}
</div>
<div class="delegates">
{#each repoDelegates as delegate}
<div class="nid">
<NodeId {baseUrl} nodeId={delegate.id} alias={delegate.alias} />
</div>
{/each}
</div>
{/if}
<div class="item-header">
<span style:text-wrap="nowrap">Seeding Scope</span>
<div class="global-flex-item">
<span class="txt-body-m-semibold">
<div class="row">
<span class="label txt-body-m-medium">Seeding Scope</span>
<span class="value txt-body-m-medium">
{capitalize(
"scope" in seedingPolicy ? seedingPolicy.scope : "not defined",
)}
</span>
<IconButton on:click={() => (policyExpanded = !policyExpanded)}>
<Icon name={policyExpanded ? "chevron-up" : "chevron-down"} />
</IconButton>
</div>
</div>
{#if policyExpanded}
<div style:color="var(--color-text-tertiary)">
<div class="description">
{#if seedingPolicy.policy === "block"}
Seeding scope only has an effect when a repository is seeded. This repo
isn't seeded by the seed node.
@@ -89,4 +100,4 @@
This repository tracks only peers followed by the seed node.
{/if}
</div>
{/if}
</div>
+24 -17
View File
@@ -10,17 +10,18 @@
import { HttpdClient } from "@http-client";
import { formatQualifiedRefname } from "@app/lib/utils";
import BlobComponent from "./Source/Blob.svelte";
import Button from "@app/components/Button.svelte";
import CloneButton from "@app/views/repos/Header/CloneButton.svelte";
import FilePath from "@app/components/FilePath.svelte";
import Header from "./Source/Header.svelte";
import Layout from "./Layout.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
import BlobComponent from "./Source/Blob.svelte";
import FilePath from "@app/components/FilePath.svelte";
import RepoNameHeader from "./Source/RepoNameHeader.svelte";
import Separator from "./Separator.svelte";
import TreeComponent from "./Source/Tree.svelte";
import { formatQualifiedRefname } from "@app/lib/utils";
export let baseUrl: BaseUrl;
export let blobResult: BlobResult;
@@ -52,6 +53,11 @@
});
};
$: currentRefname = formatQualifiedRefname(
revision || repo.payloads["xyz.radicle.project"].data.defaultBranch,
peer,
);
$: baseRoute = {
resource: "repo.source",
node: baseUrl,
@@ -68,13 +74,14 @@
.container {
display: flex;
width: inherit;
padding: 0 1rem 1rem 1rem;
padding: 0;
}
.column-left {
display: flex;
flex-direction: column;
padding-right: 0.5rem;
border-right: 1px solid var(--color-border-subtle);
}
.column-right {
@@ -82,8 +89,6 @@
flex-direction: column;
width: 100%;
padding-bottom: 2.5rem;
max-width: 75rem;
margin: 0 auto;
/* To allow pre elements to shrink when overflowing */
min-width: 0;
}
@@ -122,7 +127,6 @@
{baseUrl}
{nodeAvatarUrl}
{repo}
{seedingPolicy}
activeTab="source"
stylePaddingBottom="0">
<svelte:fragment slot="breadcrumb">
@@ -131,16 +135,19 @@
<FilePath filenameWithPath={path} />
{/if}
</svelte:fragment>
<RepoNameHeader
{repo}
currentRefname={formatQualifiedRefname(
revision || repo.payloads["xyz.radicle.project"].data.defaultBranch,
peer,
)}
{baseUrl}
slot="header" />
<svelte:fragment slot="actions">
<CloneButton
{baseUrl}
{currentRefname}
id={repo.rid}
name={repo.payloads["xyz.radicle.project"].data.name} />
</svelte:fragment>
<RepoNameHeader {repo} {baseUrl} {seedingPolicy} slot="header" />
<div style:margin="1rem" slot="subheader">
<div
style:padding="1rem"
style:border-bottom="1px solid var(--color-border-subtle)"
slot="subheader">
<Header
filesLinkActive={true}
historyLinkActive={false}
+2
View File
@@ -142,6 +142,8 @@
.markdown-wrapper {
padding: 2rem;
background-color: var(--color-surface-canvas);
max-width: 75rem;
margin: 0 auto;
}
@media (max-width: 719.98px) {
.markdown-wrapper {
+101 -64
View File
@@ -1,3 +1,8 @@
<script lang="ts" context="module">
// Cache commit counts across component remounts (tab navigation).
const commitCountCache: Record<string, number> = {};
</script>
<script lang="ts">
import type { RepoRoute } from "../router";
import type { BaseUrl, Repo, Remote, Tree } from "@http-client";
@@ -9,7 +14,7 @@
import CommitButton from "../components/CommitButton.svelte";
import Icon from "@app/components/Icon.svelte";
import Link from "@app/components/Link.svelte";
import Loading from "@app/components/Loading.svelte";
import PeerBranchSelector from "./PeerBranchSelector.svelte";
export let commit: string;
@@ -27,6 +32,22 @@
export let tree: Tree;
const api = new HttpdClient(node);
let commitCount: number | undefined = commitCountCache[commit];
function fetchCommitCount(rid: string, sha: string) {
const cached = commitCountCache[sha];
if (cached !== undefined) {
commitCount = cached;
} else {
void api.repo.getTreeStatsBySha(rid, sha).then(stats => {
commitCountCache[sha] = stats.commits;
commitCount = stats.commits;
});
}
}
$: fetchCommitCount(repo.rid, commit);
let selectedBranch: string | undefined;
let commitButtonVariant: ComponentProps<CommitButton>["variant"] | undefined =
undefined;
@@ -56,16 +77,6 @@
</script>
<style>
.top-header {
display: flex;
align-items: center;
justify-content: left;
row-gap: 0.5rem;
gap: 1px;
flex-wrap: wrap;
margin-bottom: 2rem;
}
.header {
font: var(--txt-body-s-regular);
display: flex;
@@ -73,16 +84,17 @@
align-items: center;
justify-content: left;
flex-wrap: wrap;
position: relative;
}
.header::after {
content: "";
position: absolute;
left: -1rem;
bottom: 0;
border-bottom: 1px solid var(--color-border-subtle);
width: calc(100% + 1rem);
z-index: -1;
.branch-commit {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.mobile-branch {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
.counter {
@@ -90,6 +102,8 @@
background-color: var(--color-surface-mid);
color: var(--color-text-tertiary);
padding: 0 0.25rem;
min-width: 1.5rem;
text-align: center;
}
.title-counter {
@@ -104,7 +118,7 @@
}
</style>
<div class="top-header">
<div class="mobile-branch global-hide-on-small-desktop-up" style:gap="1px">
{#if selectedBranch}
<PeerBranchSelector
{peers}
@@ -114,12 +128,75 @@
{repo}
{selectedBranch} />
{/if}
<div class="global-flex-item txt-overflow" style:gap="1px">
<CommitButton
variant={commitButtonVariant}
styleMinWidth="0"
hideSummaryOnMobile
repoId={repo.rid}
commit={lastCommit}
baseUrl={node} />
{#if !onCanonical}
<Link route={baseRoute}>
<Button
variant="not-selected"
styleBorderRadius="0 var(--border-radius-sm) var(--border-radius-sm) 0">
<Icon name="close" />
</Button>
</Link>
{/if}
</div>
<div class="header">
<div style="display: flex; gap: 0.375rem;">
<Link
route={{
resource: "repo.source",
repo: repo.rid,
node: node,
peer,
revision,
}}>
<Button variant={filesLinkActive ? "gray" : "background"}>
<Icon name="document" />Files
</Button>
</Link>
<Link
route={{
resource: "repo.history",
repo: repo.rid,
node: node,
peer,
revision,
}}>
<Button variant={historyLinkActive ? "gray" : "background"}>
<Icon name="commit" />
<div class="title-counter">
Commits
{#if commitCount !== undefined}
<div class="counter" class:selected={historyLinkActive}>
{commitCount}
</div>
{/if}
</div>
</Button>
</Link>
</div>
<div class="branch-commit global-hide-on-mobile-down" style:gap="1px">
{#if selectedBranch}
<PeerBranchSelector
{peers}
{peer}
{baseRoute}
{onCanonical}
{repo}
{selectedBranch} />
{/if}
<CommitButton
variant={commitButtonVariant}
styleMinWidth="0"
styleWidth="100%"
hideSummaryOnMobile={false}
hideSummaryOnMobile
repoId={repo.rid}
commit={lastCommit}
baseUrl={node} />
@@ -134,43 +211,3 @@
{/if}
</div>
</div>
<div class="header">
<div style="display: flex; gap: 0.375rem;">
<Link
route={{
resource: "repo.source",
repo: repo.rid,
node: node,
peer,
revision,
}}>
<Button size="large" variant={filesLinkActive ? "tab-active" : "tab"}>
<Icon name="document" />Files
</Button>
</Link>
<Link
route={{
resource: "repo.history",
repo: repo.rid,
node: node,
peer,
revision,
}}>
<Button size="large" variant={historyLinkActive ? "tab-active" : "tab"}>
<Icon name="commit" />
<div class="title-counter">
Commits
{#await api.repo.getTreeStatsBySha(repo.rid, commit)}
<Loading small center noDelay grayscale />
{:then stats}
<div class="counter" class:selected={historyLinkActive}>
{stats.commits}
</div>
{/await}
</div>
</Button>
</Link>
</div>
</div>
@@ -180,7 +180,7 @@
<DropdownListItem
selected={selectedPeer?.id === peer?.id &&
selectedBranch === revision}
style={`${subgridStyle} gap: inherit;`}>
style={subgridStyle}>
<div class="global-flex-item">
<Icon name="branch" />
<span class="txt-overflow">
@@ -231,7 +231,7 @@
</Link>
{:else}
<div
style="gap: inherit; padding: 0.5rem 0.375rem;"
style="padding: 0.5rem 0.375rem;"
class="subgrid-item txt-body-m-regular"
style:color="var(--color-text-tertiary)">
No entries found
@@ -245,9 +245,7 @@
searchInput = "";
toggle();
}}>
<DropdownListItem
selected={onCanonical}
style={`${subgridStyle} gap: inherit;`}>
<DropdownListItem selected={onCanonical} style={subgridStyle}>
<div class="global-flex-item">
<Icon name="branch" />
{repo.payloads["xyz.radicle.project"].data.defaultBranch}
@@ -68,7 +68,7 @@
peer: peer.remote.id,
revision: name,
})}
style={`${subgridStyle} padding-left: 2.3rem; gap: inherit;`}>
style={`${subgridStyle} padding-left: 2.3rem;`}>
<div class="global-flex-item">
<Icon name="branch" />
<span class="txt-overflow">
+107 -74
View File
@@ -1,32 +1,20 @@
<script lang="ts">
import type { BaseUrl, Repo } from "@http-client";
import type { BaseUrl, Repo, SeedingPolicy } from "@http-client";
import dompurify from "dompurify";
import { markdown } from "@app/lib/markdown";
import { baseUrlToString, twemoji } from "@app/lib/utils";
import { formatRepositoryId, twemoji } from "@app/lib/utils";
import Badge from "@app/components/Badge.svelte";
import CloneButton from "@app/views/repos/Header/CloneButton.svelte";
import ContextRepo from "@app/views/repos/Sidebar/ContextRepo.svelte";
import Icon from "@app/components/Icon.svelte";
import Id from "@app/components/Id.svelte";
import Link from "@app/components/Link.svelte";
import SeedButton from "@app/views/repos/Header/SeedButton.svelte";
import Share from "@app/views/repos/Share.svelte";
import RepoAvatar from "@app/components/RepoAvatar.svelte";
export let repo: Repo;
export let baseUrl: BaseUrl;
export let currentRefname: string;
let enabledArchiveDownload = false;
void fetch(
`${baseUrlToString(baseUrl)}/raw/${repo.rid}/archive/${currentRefname}`,
{
method: "HEAD",
},
).then(response => {
enabledArchiveDownload = response.ok;
});
export let seedingPolicy: SeedingPolicy;
function render(content: string): string {
return dompurify.sanitize(
@@ -38,6 +26,28 @@
</script>
<style>
.header-layout {
display: flex;
gap: 1rem;
border-bottom: 1px solid var(--color-border-subtle);
}
.avatar {
flex-shrink: 0;
line-height: 0;
}
.meta {
flex: 1;
min-width: 0;
padding: 1rem 0;
}
.info {
flex: 1;
min-width: 0;
padding: 1rem;
border-left: 1px solid var(--color-border-subtle);
display: flex;
align-items: center;
}
.title {
align-items: center;
gap: 0.5rem;
@@ -47,77 +57,100 @@
justify-content: left;
text-align: left;
text-overflow: ellipsis;
padding: 1rem 1rem 0 1rem;
}
.description {
padding: 0 1rem 1rem 1rem;
}
.repo-name:hover {
color: inherit;
}
.description {
margin-top: 0.5rem;
font: var(--txt-body-m-regular);
color: var(--color-text-tertiary);
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.description :global(a) {
border-bottom: 1px solid var(--color-text-tertiary);
}
.description :global(a:hover) {
border-bottom: 1px solid var(--color-text-primary);
}
.id {
padding-left: 1rem;
}
.title-container {
display: flex;
flex-direction: column;
gap: 0rem;
margin-bottom: 1rem;
@media (max-width: 719.98px) {
.header-layout {
flex-wrap: wrap;
padding: 1rem;
}
.avatar :global(img) {
width: 4rem !important;
}
.meta {
flex-basis: 0;
padding: 0;
}
.info {
flex-basis: 100%;
padding: 0;
border-left: none;
border-top: 1px solid var(--color-border-subtle);
padding-top: 0.5rem;
}
.mobile-description {
flex-basis: 100%;
}
}
</style>
<div class="title-container">
<div class="title">
<span class="txt-overflow">
<Link
route={{
resource: "repo.source",
repo: repo.rid,
node: baseUrl,
}}>
<span class="repo-name">
{project.data.name}
</span>
</Link>
</span>
{#if repo.visibility.type === "private"}
<Badge variant="private" size="tiny">
<Icon name="lock" />
Private
</Badge>
{/if}
<div style="margin-left: auto; display: flex; gap: 0.5rem;">
<Share />
<div
style:display="flex"
style:gap="0.5rem"
class="global-hide-on-mobile-down">
<CloneButton
{enabledArchiveDownload}
{baseUrl}
{currentRefname}
id={repo.rid}
name={project.data.name} />
<SeedButton seedCount={repo.seeding} repoId={repo.rid} />
</div>
<div
style:display="flex"
style:gap="0.5rem"
class="global-hide-on-small-desktop-up">
<SeedButton disabled seedCount={repo.seeding} repoId={repo.rid} />
</div>
<div class="header-layout">
<div class="avatar">
<RepoAvatar name={project.data.name} rid={repo.rid} styleWidth="10rem" />
</div>
<div class="meta">
<div class="title">
<span class="txt-overflow">
<Link
route={{
resource: "repo.source",
repo: repo.rid,
node: baseUrl,
}}>
<span class="repo-name">
{project.data.name}
</span>
</Link>
</span>
{#if repo.visibility.type === "private"}
<Badge variant="private" size="tiny">
<Icon name="lock" />
Private
</Badge>
{/if}
</div>
<div>
<Id shorten={false} id={repo.rid} ariaLabel="repo-id">
{formatRepositoryId(repo.rid)}
</Id>
</div>
<div
class="description global-hide-on-mobile-down"
title={project.data.description}
use:twemoji>
{@html render(project.data.description)}
</div>
</div>
<div class="id">
<Id shorten={false} id={repo.rid} ariaLabel="repo-id" />
<div
class="description mobile-description global-hide-on-small-desktop-up"
title={project.data.description}
use:twemoji>
{@html render(project.data.description)}
</div>
<div class="info">
<ContextRepo
{baseUrl}
repoThreshold={repo.threshold}
repoDelegates={repo.delegates}
{seedingPolicy} />
</div>
</div>
<div class="description" use:twemoji>
{@html render(project.data.description)}
</div>
+5 -23
View File
@@ -145,7 +145,6 @@ export type RepoLoadedRoute =
resource: "repo.commit";
params: {
baseUrl: BaseUrl;
seedingPolicy: SeedingPolicy;
repo: Repo;
commit: Commit;
nodeAvatarUrl: string | undefined;
@@ -155,7 +154,6 @@ export type RepoLoadedRoute =
resource: "repo.issue";
params: {
baseUrl: BaseUrl;
seedingPolicy: SeedingPolicy;
repo: Repo;
rawPath: (commit?: string) => string;
issue: Issue;
@@ -166,7 +164,6 @@ export type RepoLoadedRoute =
resource: "repo.issues";
params: {
baseUrl: BaseUrl;
seedingPolicy: SeedingPolicy;
repo: Repo;
issues: Issue[];
status: IssueState["status"];
@@ -177,7 +174,6 @@ export type RepoLoadedRoute =
resource: "repo.patches";
params: {
baseUrl: BaseUrl;
seedingPolicy: SeedingPolicy;
repo: Repo;
patches: Patch[];
status: PatchState["status"];
@@ -188,7 +184,6 @@ export type RepoLoadedRoute =
resource: "repo.patch";
params: {
baseUrl: BaseUrl;
seedingPolicy: SeedingPolicy;
repo: Repo;
rawPath: (commit?: string) => string;
patch: Patch;
@@ -270,10 +265,9 @@ export async function loadRepoRoute(
} else if (route.resource === "repo.history") {
return await loadHistoryView(route, previousLoaded);
} else if (route.resource === "repo.commit") {
const [repo, commit, seedingPolicy, node] = await Promise.all([
const [repo, commit, node] = await Promise.all([
api.repo.getByRid(route.repo),
api.repo.getCommitBySha(route.repo, route.commit),
api.getPolicyByRid(route.repo),
api.getNode(),
]);
@@ -281,7 +275,6 @@ export async function loadRepoRoute(
resource: "repo.commit",
params: {
baseUrl: route.node,
seedingPolicy,
repo,
commit,
nodeAvatarUrl: node.avatarUrl,
@@ -318,14 +311,13 @@ async function loadPatchesView(
const searchParams = new URLSearchParams(route.search || "");
const status = (searchParams.get("status") as PatchState["status"]) || "open";
const [repo, patches, seedingPolicy, node] = await Promise.all([
const [repo, patches, node] = await Promise.all([
api.repo.getByRid(route.repo),
api.repo.getAllPatches(route.repo, {
status,
page: 0,
perPage: PATCHES_PER_PAGE,
}),
api.getPolicyByRid(route.repo),
api.getNode(),
]);
@@ -333,7 +325,6 @@ async function loadPatchesView(
resource: "repo.patches",
params: {
baseUrl: route.node,
seedingPolicy,
patches,
status,
repo,
@@ -348,14 +339,13 @@ async function loadIssuesView(
const api = new HttpdClient(route.node);
const status = route.status || "open";
const [repo, issues, seedingPolicy, node] = await Promise.all([
const [repo, issues, node] = await Promise.all([
api.repo.getByRid(route.repo),
api.repo.getAllIssues(route.repo, {
status,
page: 0,
perPage: ISSUES_PER_PAGE,
}),
api.getPolicyByRid(route.repo),
api.getNode(),
]);
@@ -363,7 +353,6 @@ async function loadIssuesView(
resource: "repo.issues",
params: {
baseUrl: route.node,
seedingPolicy,
issues,
status,
repo,
@@ -623,17 +612,15 @@ async function loadIssueView(route: RepoIssueRoute): Promise<RepoLoadedRoute> {
route.repo
}${commit ? `/${commit}` : ""}`;
const [repo, issue, seedingPolicy, node] = await Promise.all([
const [repo, issue, node] = await Promise.all([
api.repo.getByRid(route.repo),
api.repo.getIssueById(route.repo, route.issue),
api.getPolicyByRid(route.repo),
api.getNode(),
]);
return {
resource: "repo.issue",
params: {
baseUrl: route.node,
seedingPolicy,
repo,
rawPath,
issue,
@@ -655,7 +642,6 @@ async function loadPatchView(
let repoPromise: Promise<Repo>;
let patchPromise: Promise<Patch>;
let nodePromise: Promise<Partial<Node>>;
let seedingPolicyPromise: Promise<SeedingPolicy>;
if (
previousLoaded.resource === "repo.patch" &&
@@ -664,20 +650,17 @@ async function loadPatchView(
) {
repoPromise = Promise.resolve(previousLoaded.params.repo);
patchPromise = Promise.resolve(previousLoaded.params.patch);
seedingPolicyPromise = Promise.resolve(previousLoaded.params.seedingPolicy);
nodePromise = Promise.resolve({
avatarUrl: previousLoaded.params.nodeAvatarUrl,
});
} else {
repoPromise = api.repo.getByRid(route.repo);
patchPromise = api.repo.getPatchById(route.repo, route.patch);
seedingPolicyPromise = api.getPolicyByRid(route.repo);
nodePromise = api.getNode();
}
const [repo, patch, seedingPolicy, { avatarUrl }] = await Promise.all([
const [repo, patch, { avatarUrl }] = await Promise.all([
repoPromise,
patchPromise,
seedingPolicyPromise,
nodePromise,
]);
@@ -740,7 +723,6 @@ async function loadPatchView(
resource: "repo.patch",
params: {
baseUrl: route.node,
seedingPolicy,
repo,
rawPath,
patch,
+1 -2
View File
@@ -69,8 +69,7 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
await page.getByRole("button", { name: "59a0821", exact: true }).click();
await expectClipboard("59a0821edc73630bce540596cffc7854da557365");
await page.getByLabel("filter-dropdown").click();
await page.getByRole("button", { name: "Draft" }).click();
await page.getByRole("link", { name: "Draft" }).click();
await page.getByRole("button", { name: "783d33c", exact: true }).click();
await expectClipboard("783d33c5b14e13234d4d7affa98bd0b52d1b1ea3");
}
+32 -15
View File
@@ -21,10 +21,10 @@ test("navigate to repo", async ({ page }) => {
// Header.
{
const name = page.getByRole("link", { name: "source-browsing" }).nth(1);
const id = page.getByText(sourceBrowsingRid);
const description = page.getByText(
"Git repository for source browsing tests",
);
const id = page.getByLabel("repo-id");
const description = page
.getByText("Git repository for source browsing tests")
.first();
await expect(name).toBeVisible();
await expect(id).toBeVisible();
@@ -33,7 +33,9 @@ test("navigate to repo", async ({ page }) => {
// Repo menu shows default selected branch and commit and contributor counts.
{
await expect(page.getByTitle("Change branch")).toBeVisible();
await expect(
page.locator('[title="Change branch"]:visible').first(),
).toBeVisible();
await expect(
page
.getByRole("button", {
@@ -66,7 +68,7 @@ test("repo description", async ({ page, peer }) => {
await page.goto(peer.ridUrl(rid));
await page.waitForLoadState("networkidle");
await expect(
page.getByText("Radicle Heartwood Protocol & Stack"),
page.getByText("Radicle Heartwood Protocol & Stack").first(),
).toBeVisible();
});
@@ -83,7 +85,9 @@ test("show source tree at specific revision", async ({ page }) => {
})
.click();
await expect(page.getByTitle("Current HEAD")).toContainText("335dd6d");
await expect(page.getByTitle("Current HEAD").first()).toContainText(
"335dd6d",
);
await expect(page.locator(".source-tree")).toHaveText("bin src");
await expect(
page.getByRole("link", {
@@ -317,11 +321,15 @@ test("peer and branch switching", async ({ page }) => {
// Alice's peer.
{
await changeBranch("alice", `main ${shortAliceHead}`, page);
await expect(page.getByTitle("Change branch")).toHaveText(/alice/);
await expect(
page.locator('[title="Change branch"]:visible').first(),
).toHaveText(/alice/);
// Default `main` branch.
{
await expect(page.getByTitle("Change branch")).toHaveText(/main/);
await expect(
page.locator('[title="Change branch"]:visible').first(),
).toHaveText(/main/);
await expect(
page
.getByRole("button", {
@@ -339,8 +347,11 @@ test("peer and branch switching", async ({ page }) => {
// Feature branch with a slash in the name.
{
await changeBranch("alice", "feature/branch", page);
await page.getByTitle("Change branch").click();
await page.getByText("feature/branch").click();
await page.locator('[title="Change branch"]:visible').first().click();
await page
.getByRole("button", { name: "feature/branch" })
.first()
.click();
await expect(
page.getByRole("button", { name: "feature/branch" }),
@@ -379,10 +390,16 @@ test("peer and branch switching", async ({ page }) => {
{
await page.getByRole("link", { name: "source-browsing" }).nth(1).click();
await expect(page.getByTitle("Change branch")).not.toContainText("alice");
await expect(page.getByTitle("Change branch")).not.toContainText("bob");
await expect(
page.locator('[title="Change branch"]:visible').first(),
).not.toContainText("alice");
await expect(
page.locator('[title="Change branch"]:visible').first(),
).not.toContainText("bob");
await expect(page.getByTitle("Change branch")).toBeVisible();
await expect(
page.locator('[title="Change branch"]:visible').first(),
).toBeVisible();
await expect(
page
.getByRole("button", {
@@ -437,7 +454,7 @@ test("only one modal can be open at a time", async ({ page }) => {
await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
await expect(page.getByText("bob")).not.toBeVisible();
await page.getByTitle("Change branch").click();
await page.locator('[title="Change branch"]:visible').first().click();
await expect(page.getByText("Code font")).not.toBeVisible();
await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
await expect(page.getByText("bob")).toBeVisible();
+2 -5
View File
@@ -14,7 +14,7 @@ test("navigation from commit list", async ({ page }) => {
await page.goto(sourceBrowsingUrl);
await changeBranch("bob", `main ${shortBobHead}`, page);
await page.getByText("Update readme").first().click();
await page.getByRole("link", { name: "Update readme" }).first().click();
await expect(page).toHaveURL(commitUrl);
});
@@ -103,13 +103,10 @@ test("navigation to source tree at specific revision", async ({ page }) => {
// Go to source tree at this revision.
await page.getByTitle("View file at this commit").click();
const branchSelectorCommitButton = page.getByTitle("Current HEAD").first();
await expect(
branchSelectorCommitButton.getByText("Add a deeply nested directory tree"),
).toBeVisible();
await expect(branchSelectorCommitButton).toContainText("0801ace");
await expect(page).toHaveURL(
`${sourceBrowsingUrl}/tree/0801aceeab500033f8d608778218657bd626ef73/deep/directory/hierarchy/is/entirely/possible/in/git/repositories/.gitkeep`,
);
await expect(branchSelectorCommitButton).toContainText("0801ace");
await expect(page.locator(".source-tree >> text=.gitkeep")).toBeVisible();
await expect(
page
+12 -6
View File
@@ -23,9 +23,9 @@ test("peer and branch switching", async ({ page }) => {
{
await changeBranch("alice", `main ${shortAliceHead}`, page);
await expect(page.getByTitle("Change branch")).toHaveText(
"alice Delegate / main",
);
await expect(
page.locator('[title="Change branch"]:visible').first(),
).toHaveText("alice Delegate / main");
await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
await expect(page.locator(".list .teaser")).toHaveCount(
@@ -63,7 +63,9 @@ test("peer and branch switching", async ({ page }) => {
{
await changeBranch("bob", `main ${shortBobHead}`, page);
await expect(page.getByTitle("Change branch")).toContainText("bob");
await expect(
page.locator('[title="Change branch"]:visible').first(),
).toContainText("bob");
await expect(page.getByText("Wednesday, December 21, 2022")).toBeVisible();
await expect(page.locator(".list").first().locator(".teaser")).toHaveCount(
@@ -158,7 +160,9 @@ test("relative timestamps", async ({ page }) => {
await expect(page.getByText("Thursday, December 15,")).toBeVisible();
await changeBranch("bob", `main ${shortBobHead}`, page);
await expect(page.getByTitle("Change branch")).toHaveText(/bob/);
await expect(
page.locator('[title="Change branch"]:visible').first(),
).toHaveText(/bob/);
const latestCommit = page.locator(".teaser").first();
await expect(latestCommit).toContainText(
`Bob Belcher committed ${shortBobHead} now`,
@@ -194,7 +198,9 @@ test("pushing changes while viewing history", async ({ page, peerManager }) => {
await expect(page).toHaveURL(`${alice.uiUrl()}/${rid}/history`);
await expect(page.getByRole("link", { name: "Commits 2" })).toBeVisible();
await expect(page.getByTitle("Change branch")).toHaveText("main Canonical");
await expect(
page.locator('[title="Change branch"]:visible').first(),
).toHaveText("main Canonical");
const branchSelectorCommitButton = page.getByTitle("Current HEAD").first();
await expect(branchSelectorCommitButton).toHaveText("516fa74 first change");
+2 -7
View File
@@ -7,8 +7,7 @@ test("navigate issue listing", async ({ page }) => {
await page.getByRole("link", { name: "Issues 1" }).click();
await expect(page).toHaveURL(`${cobUrl}/issues`);
await page.getByRole("button", { name: "filter-dropdown" }).first().click();
await page.getByRole("link", { name: "Closed 2" }).click();
await page.getByRole("link", { name: "Closed" }).click();
await expect(page).toHaveURL(`${cobUrl}/issues?status=closed`);
});
@@ -39,12 +38,8 @@ test("issue counters", async ({ page, peer }) => {
],
{ cwd: repoFolder },
);
await page.getByRole("button", { name: "filter-dropdown" }).first().click();
await page.locator(".dropdown-item").getByText("Open 1").click();
await page.getByRole("link", { name: "Open" }).first().click();
await expect(page.getByRole("button", { name: "Issues 2" })).toBeVisible();
await expect(
page.getByRole("button", { name: "filter-dropdown" }).first(),
).toHaveText("Open 2");
await expect(page.locator(".issue-teaser")).toHaveCount(2);
await page
+2 -7
View File
@@ -6,8 +6,7 @@ test("navigate patch listing", async ({ page }) => {
await page.getByRole("link", { name: "Patches 2" }).click();
await expect(page).toHaveURL(`${cobUrl}/patches`);
await page.getByRole("button", { name: "filter-dropdown" }).first().click();
await page.getByRole("link", { name: "Merged 1" }).click();
await page.getByRole("link", { name: "Merged" }).click();
await expect(page).toHaveURL(`${cobUrl}/patches?status=merged`);
await expect(
page.locator(".comments").filter({ hasText: "5" }),
@@ -40,11 +39,7 @@ test("patches counters", async ({ page, peer }) => {
await peer.git(["push", "rad", "HEAD:refs/patches"], {
cwd: repoFolder,
});
await page.getByRole("button", { name: "filter-dropdown" }).first().click();
await page.locator(".dropdown-item").getByText("Open 1").click();
await page.getByRole("link", { name: "Open" }).first().click();
await expect(page.getByRole("button", { name: "Patches 2" })).toBeVisible();
await expect(
page.getByRole("button", { name: "filter-dropdown" }).first(),
).toHaveText("Open 2");
await expect(page.locator(".patch-teaser")).toHaveCount(2);
});
+1 -1
View File
@@ -4,7 +4,7 @@ import type { RadiclePeer } from "@tests/support/peerManager";
import * as Path from "node:path";
export async function changeBranch(peer: string, branch: string, page: Page) {
await page.getByTitle("Change branch").click();
await page.locator('[title="Change branch"]:visible').first().click();
const peerLocator = page.getByLabel("peer-item").filter({ hasText: peer });
await peerLocator.getByTitle("Expand peer").click();
await page.getByRole("button", { name: branch }).click();