Overhaul app responsiveness

We introduce more breakpoints to cover mobile, small-desktop,
medium-desktop and large-desktop viewports.
This commit is contained in:
Rūdolfs Ošiņš
2024-05-06 16:43:05 +02:00
parent 9404c3092a
commit 698a833e42
52 changed files with 966 additions and 671 deletions
+37 -4
View File
@@ -97,17 +97,50 @@ pre {
background-color: var(--color-fill-ghost);
}
@media (max-width: 720px) {
/*
Breakpoints
===========
mobile 0px - 719.98px
small desktop 720px - 1010.98px
medium desktop 1011px - 1349.98px
desktop 1350px - ∞ px
*/
@media (max-width: 719.98px) {
body {
min-width: 0;
}
.global-hide-on-mobile {
.global-hide-on-mobile-down {
display: none !important;
}
}
@media (min-width: 721px) {
.global-hide-on-desktop {
@media (max-width: 1010.98px) {
.global-hide-on-small-desktop-down {
display: none !important;
}
}
@media (max-width: 1349.98px) {
.global-hide-on-medium-desktop-down {
display: none !important;
}
}
@media (min-width: 720px) {
.global-hide-on-small-desktop-up {
display: none !important;
}
}
@media (min-width: 1011px) {
.global-hide-on-medium-desktop-up {
display: none !important;
}
}
@media (min-width: 1350px) {
.global-hide-on-desktop-up {
display: none !important;
}
}
+4 -4
View File
@@ -17,7 +17,7 @@
height: 100%;
overflow-y: scroll;
}
@media (max-width: 720px) {
@media (max-width: 719.98px) {
.app {
display: grid;
grid-template-rows: 1fr auto;
@@ -30,17 +30,17 @@
</style>
<div class="app">
<div class="global-hide-on-mobile header">
<div class="global-hide-on-mobile-down header">
<Header />
</div>
<div class="content">
<slot />
</div>
<div style:margin-top="auto">
<div class="global-hide-on-mobile">
<div class="global-hide-on-mobile-down">
<Footer />
</div>
<div class="global-hide-on-desktop">
<div class="global-hide-on-small-desktop-up">
<MobileFooter />
</div>
</div>
+1 -1
View File
@@ -40,7 +40,7 @@
<div class="footer">
<div class="left">
<Popover popoverPositionBottom="2rem" popoverPositionLeft="0">
<Popover popoverPositionBottom="3rem" popoverPositionLeft="0">
<IconButton slot="toggle" let:toggle on:click={toggle}>
<IconSmall name="settings" />
Settings
+1 -1
View File
@@ -41,7 +41,7 @@
</style>
{#if $httpdStore.state === "authenticated"}
<Popover popoverPositionTop="3rem" popoverPositionRight="0">
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
<Button slot="toggle" let:toggle on:click={toggle} variant="naked-toggle">
<div class="peer-info">
<div style:height="1.25rem" style:margin-right="0.5rem">
+1
View File
@@ -17,6 +17,7 @@
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-small);
white-space: nowrap;
flex-wrap: wrap;
}
</style>
+1 -1
View File
@@ -29,7 +29,7 @@
}
</style>
<Popover popoverPositionTop="3rem" popoverPositionRight="0">
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
<Button slot="toggle" let:toggle on:click={toggle} variant={"naked-toggle"}>
{#if node.state === "running"}
<IconSmall name="online" />
+2 -2
View File
@@ -7,9 +7,7 @@
<style>
.help {
width: 18.5rem;
font-size: var(--font-size-small);
color: var(--color-foreground-dim);
display: flex;
flex-direction: column;
gap: 1rem;
@@ -18,6 +16,8 @@
display: flex;
justify-content: space-between;
width: 100%;
white-space: nowrap;
gap: 2rem;
}
.divider {
border-bottom: 1px solid var(--color-fill-separator);
+3 -2
View File
@@ -16,7 +16,6 @@
<style>
.settings {
width: 24rem;
display: flex;
flex-direction: column;
align-items: center;
@@ -28,6 +27,8 @@
display: flex;
width: 100%;
align-items: center;
gap: 2rem;
white-space: nowrap;
}
.right {
@@ -78,7 +79,7 @@
</Radio>
</div>
</div>
<div class="item">
<div class="item global-hide-on-mobile-down">
<div
style="display: flex; flex-direction: row; align-items: center; gap: 0.5rem;">
Make changes on the web (experimental)
+16 -13
View File
@@ -54,8 +54,10 @@
.card-header {
display: flex;
align-items: center;
white-space: nowrap;
flex-wrap: wrap;
padding: 0 0.75rem;
height: 1.5rem;
min-height: 1.5rem;
gap: 0.5rem;
font-size: var(--font-size-small);
}
@@ -143,19 +145,18 @@
{utils.formatTimestamp(timestamp)}
</span>
{#if lastEdit}
<div class="card-metadata">•</div>
<div
class="card-metadata"
title={utils.formatEditedCaption(
lastEdit.author,
lastEdit.timestamp,
)}>
edited
• edited
</div>
{/if}
<div class="header-right">
{#if id && editComment && state === "read"}
<div class="edit-buttons">
<div class="edit-buttons global-hide-on-mobile-down">
<IconButton title="edit comment" on:click={() => (state = "edit")}>
<IconSmall name={"edit"} />
</IconButton>
@@ -199,15 +200,17 @@
<div class="actions">
{#if id && reactOnComment}
{@const reactOnComment_ = reactOnComment}
<ReactionSelector
{reactions}
on:select={async ({ detail: { authors, emoji } }) => {
try {
await reactOnComment_(authors, emoji);
} finally {
closeFocused();
}
}} />
<div class="global-hide-on-mobile-down">
<ReactionSelector
{reactions}
on:select={async ({ detail: { authors, emoji } }) => {
try {
await reactOnComment_(authors, emoji);
} finally {
closeFocused();
}
}} />
</div>
{/if}
{#if id && reactions && reactions.length > 0}
<Reactions handleReaction={reactOnComment} {reactions} />
+10 -2
View File
@@ -42,6 +42,14 @@
font-size: var(--font-size-small);
text-align: center;
}
.command {
max-width: 25rem;
}
@media (max-width: 719.98px) {
.command {
max-width: 20rem;
}
}
</style>
<div class="error">
@@ -54,13 +62,13 @@
{#if error}
<div class="help">
If you need help resolving this issue, copy the error message
<br />
<br class="global-hide-on-mobile-down" />
below and send it to us on
<ExternalLink href={config.supportWebsite}>
{config.supportWebsite}
</ExternalLink>
</div>
<div style:max-width="25rem">
<div class="command">
<Command
command={JSON.stringify(error, Object.getOwnPropertyNames(error))}
fullWidth
+13
View File
@@ -56,6 +56,19 @@
border-bottom-left-radius: var(--border-radius-small);
border-bottom-right-radius: var(--border-radius-small);
}
@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>
+1
View File
@@ -42,4 +42,5 @@
class:txt-small={fontSize === "small"}
class:txt-tiny={fontSize === "tiny"}>
{@html render(content)}
<slot />
</span>
+1
View File
@@ -215,6 +215,7 @@
}
.markdown {
max-width: 1024px;
word-break: break-word;
}
.front-matter {
font-size: var(--font-size-tiny);
+1
View File
@@ -34,6 +34,7 @@
font-family: var(--font-family-monospace);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-small);
white-space: nowrap;
}
.popover-container {
display: flex;
+6 -1
View File
@@ -67,6 +67,11 @@
.reply {
padding: 1rem;
}
@media (max-width: 719.98px) {
.comments {
border-radius: 0;
}
}
</style>
<div class="comments">
@@ -115,7 +120,7 @@
</div>
{/if}
{#if createReply}
<div id={`reply-${root.id}`} class="reply">
<div id={`reply-${root.id}`} class="reply global-hide-on-mobile-down">
<CommentToggleInput
{rawPath}
focus
+6 -9
View File
@@ -143,7 +143,7 @@
text-overflow: ellipsis;
}
@media (max-width: 720px) {
@media (max-width: 719.98px) {
.wrapper {
width: 100%;
padding: 1rem;
@@ -156,9 +156,9 @@
</style>
<AppLayout>
<div class="wrapper">
<div class="wrapper" style:padding-bottom="2.5rem">
{#if nodeId}
<div class="global-hide-on-mobile">
<div class="global-hide-on-mobile-down">
<HomepageSection
loading={$httpdStore.state !== "stopped" &&
localProjects === undefined}
@@ -235,18 +235,15 @@
</div>
{/if}
{#if !nodeId}
<div class="global-hide-on-mobile">
<Popover
popoverPositionTop="1.5rem"
popoverPositionLeft="0"
popoverPositionRight="-15rem">
<div class="global-hide-on-mobile-down">
<Popover popoverPositionTop="2.5rem" popoverPositionLeft="0">
<IconButton slot="toggle" let:toggle on:click={toggle}>
<span style:color="var(--color-fill-gray)">
<IconSmall name="info" />
</span>
</IconButton>
<div slot="popover" class="popover txt-small">
<div slot="popover" class="popover txt-small" style:width="15rem">
<div style:padding-bottom="0.5rem">
To browse your local projects, run:
</div>
+1 -1
View File
@@ -47,7 +47,7 @@
<Popover
{popoverPositionRight}
{popoverPositionLeft}
popoverPositionBottom="1.5rem">
popoverPositionBottom="2.5rem">
<IconButton slot="toggle" let:toggle on:click={toggle}>
<span style:color="var(--color-fill-gray)">
<IconSmall name="info" />
+7 -7
View File
@@ -116,7 +116,7 @@
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
}
@media (max-width: 720px) {
@media (max-width: 719.98px) {
.wrapper {
width: 100%;
padding: 1rem;
@@ -147,10 +147,10 @@
<!-- else this is probably a local node -->
<!-- So we show only the nid -->
<CopyableId id={nid} style="oid">
<div class="global-hide-on-desktop">
<div class="global-hide-on-small-desktop-up">
{truncateId(nid)}
</div>
<div class="global-hide-on-mobile">
<div class="global-hide-on-mobile-down">
{nid}
</div>
</CopyableId>
@@ -166,19 +166,19 @@
<div class="txt-semibold">
{isLocal(baseUrl.hostname) ? "Seeded" : "Pinned"} projects
</div>
<div class="global-hide-on-mobile" style:margin-left="auto">
<div class="global-hide-on-mobile-down" style:margin-left="auto">
{#if policy && scope}
<ScopePolicyPopover {scope} {policy} popoverPositionRight="0" />
{/if}
</div>
</div>
<div class="subtitle global-hide-on-desktop">
<div class="subtitle global-hide-on-small-desktop-up">
{#if policy && scope}
<ScopePolicyPopover {scope} {policy} popoverPositionLeft="-5.5rem" />
<ScopePolicyPopover {scope} {policy} popoverPositionRight="-4.5rem" />
{/if}
</div>
<div style:margin-top="1rem">
<div style:margin-top="1rem" style:padding-bottom="2.5rem">
{#await fetchProjectInfos( baseUrl, { show: isLocal(baseUrl.hostname) ? "all" : "pinned", perPage: stats.repos.total }, )}
<div style:height="35vh">
<Loading small center />
+20 -10
View File
@@ -3,8 +3,9 @@
import FileDiff from "@app/views/projects/Changeset/FileDiff.svelte";
import FileLocationChange from "@app/views/projects/Changeset/FileLocationChange.svelte";
import Observer, { intersection } from "@app/components/Observer.svelte";
import IconButton from "@app/components/IconButton.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import Observer, { intersection } from "@app/components/Observer.svelte";
export let diff: Diff;
export let files: Record<string, CommitBlob>;
@@ -58,12 +59,21 @@
.summary {
font-size: var(--font-size-small);
}
@media (max-width: 719.98px) {
.diff-list {
padding: 1rem 0;
}
.header {
align-items: flex-start;
}
}
</style>
<div class="header">
<div class="summary">
<span>{diffDescription(diff.files)}</span>
with
<br class="global-hide-on-small-desktop-up" />
<span class:additions={diff.stats.insertions > 0}>
{diff.stats.insertions}
{pluralize("insertion", diff.stats.insertions)}
@@ -75,15 +85,15 @@
</span>
</div>
{#if diff.stats.filesChanged > 1}
<div style:display="flex" style:gap="1rem">
<IconButton on:click={() => (expanded = !expanded)}>
{#if expanded === true}
Collapse all
{:else}
Expand all
{/if}
</IconButton>
</div>
<IconButton on:click={() => (expanded = !expanded)}>
{#if expanded === true}
<IconSmall name="collapse" />
<span class="global-hide-on-mobile-down">Collapse all</span>
{:else}
<IconSmall name="expand" />
<span class="global-hide-on-mobile-down">Expand all</span>
{/if}
</IconButton>
{/if}
</div>
+33 -10
View File
@@ -81,6 +81,7 @@
flex-wrap: wrap;
flex-direction: row;
gap: 0.5rem;
font-size: var(--font-size-small);
}
.assignee {
display: flex;
@@ -96,9 +97,29 @@
position: relative;
margin-top: 0.5rem;
}
@media (max-width: 1349.98px) {
.wrapper {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: flex-start;
}
.header {
margin-bottom: 0;
height: 2rem;
display: flex;
align-items: center;
}
.no-assignees {
height: 2rem;
display: flex;
align-items: center;
}
}
</style>
<div>
<div class="wrapper">
<div class="header">Assignees</div>
<div class="body">
{#if locallyAuthenticated}
@@ -150,14 +171,16 @@
</div>
{/if}
{:else}
<Badge
variant="outline"
size="small"
title="add assignee"
round
on:click={() => (showInput = true)}>
<IconSmall name="plus" />
</Badge>
<div class="global-hide-on-mobile-down">
<Badge
variant="outline"
size="small"
title="add assignee"
round
on:click={() => (showInput = true)}>
<IconSmall name="plus" />
</Badge>
</div>
{/if}
{:else}
{#each updatedAssignees as assignee}
@@ -168,7 +191,7 @@
</div>
</Badge>
{:else}
<div class="txt-missing">No assignees</div>
<div class="txt-missing no-assignees">No assignees</div>
{/each}
{/if}
</div>
+10 -1
View File
@@ -50,6 +50,10 @@
margin: 0.5rem 0;
font-size: var(--font-size-tiny);
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
<div class="teaser" aria-label="commit-teaser">
@@ -81,10 +85,15 @@
<pre>{commit.description.trim()}</pre>
</div>
{/if}
<div class="global-hide-on-small-desktop-up">
<CompactCommitAuthorship {commit} />
</div>
</div>
<div class="right">
<div style="display: flex; gap: 0.5rem; height: 21px; align-items: center;">
<CompactCommitAuthorship {commit} />
<div class="global-hide-on-mobile-down">
<CompactCommitAuthorship {commit} />
</div>
<IconButton title="Browse repo at this commit">
<Link
route={{
+2 -2
View File
@@ -15,7 +15,7 @@
}
.summary {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.5rem;
@@ -36,8 +36,8 @@
</div>
<div class="subtitle">
<slot name="state" />
<slot name="author" />
</div>
<slot name="subtitle" />
<div class="description">
<slot name="description" />
</div>
+17 -8
View File
@@ -9,9 +9,6 @@
<style>
.header {
display: flex;
gap: 1rem;
align-items: center;
font-size: var(--font-size-small);
margin-bottom: 0.75rem;
}
@@ -20,14 +17,26 @@
flex-wrap: wrap;
flex-direction: row;
gap: 0.5rem;
margin-bottom: 1.25rem;
font-size: var(--font-size-small);
}
@media (max-width: 1349.98px) {
.wrapper {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: flex-start;
}
.header {
margin-bottom: 0;
display: flex;
align-items: center;
}
}
</style>
<div>
<div class="header">
<span>Attachments</span>
</div>
<div class="wrapper">
<div class="header">Attachments</div>
<div class="body">
{#each embeds as embed}
<Badge variant="neutral">
+36 -14
View File
@@ -57,16 +57,17 @@
</script>
<style>
.metadata-section-header {
.header {
font-size: var(--font-size-small);
margin-bottom: 0.75rem;
}
.metadata-section-body {
.body {
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: row;
gap: 0.5rem;
font-size: var(--font-size-small);
}
.validation-message {
display: flex;
@@ -76,11 +77,30 @@
position: relative;
margin-top: 0.5rem;
}
@media (max-width: 1349.98px) {
.wrapper {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: flex-start;
}
.header {
margin-bottom: 0;
height: 2rem;
display: flex;
align-items: center;
}
.no-labels {
height: 2rem;
display: flex;
align-items: center;
}
}
</style>
<div>
<div class="metadata-section-header">Labels</div>
<div class="metadata-section-body">
<div class="wrapper">
<div class="header">Labels</div>
<div class="body">
{#if locallyAuthenticated}
{#each updatedLabels as label}
<Badge
@@ -124,14 +144,16 @@
</div>
{/if}
{:else}
<Badge
variant="outline"
size="small"
title="add labels"
round
on:click={() => (showInput = true)}>
<IconSmall name="plus"></IconSmall>
</Badge>
<div class="global-hide-on-mobile-down">
<Badge
variant="outline"
size="small"
title="add labels"
round
on:click={() => (showInput = true)}>
<IconSmall name="plus"></IconSmall>
</Badge>
</div>
{/if}
{:else}
{#each updatedLabels as label}
@@ -139,7 +161,7 @@
{label}
</Badge>
{:else}
<div class="txt-missing">No labels</div>
<div class="txt-missing no-labels">No labels</div>
{/each}
{/if}
</div>
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
import Badge from "@app/components/Badge.svelte";
export let labels: string[];
</script>
<style>
.label {
overflow: hidden;
text-overflow: ellipsis;
}
</style>
{#each labels.slice(0, 4) as label}
<Badge style="max-width:7rem" variant="neutral">
<span class="label">{label}</span>
</Badge>
{/each}
{#if labels.length > 4}
<Badge title={labels.slice(4, undefined).join(" ")} variant="neutral">
<span class="label">
+{labels.length - 4} more label{labels.length > 5 ? "s" : ""}
</span>
</Badge>
{/if}
+75
View File
@@ -0,0 +1,75 @@
<script lang="ts">
import type { PatchReviews } from "../Patch.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import NodeId from "@app/components/NodeId.svelte";
export let reviews: PatchReviews;
</script>
<style>
.header {
font-size: var(--font-size-small);
margin-bottom: 0.75rem;
}
.body {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: var(--font-size-small);
}
.review {
color: var(--color-fill-gray);
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.review-accept {
color: var(--color-foreground-success);
}
.review-reject {
color: var(--color-foreground-red);
}
@media (max-width: 1349.98px) {
.wrapper {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
}
.header {
margin-bottom: 0;
}
.no-reviews {
display: flex;
align-items: center;
}
.body {
flex-direction: row;
}
}
</style>
<div class="wrapper">
<div class="header">Reviews</div>
<div class="body">
{#each Object.values(reviews) as { latest, review }}
<div class="review" class:txt-missing={!latest}>
<span
class:review-accept={review.verdict === "accept"}
class:review-reject={review.verdict === "reject"}>
{#if review.verdict === "accept"}
<IconSmall name="checkmark" />
{:else if review.verdict === "reject"}
<IconSmall name="cross" />
{:else}
<IconSmall name="chat" />
{/if}
</span>
<NodeId nodeId={review.author.id} alias={review.author.alias} />
</div>
{:else}
<div class="txt-missing no-reviews">No reviews</div>
{/each}
</div>
</div>
+34 -18
View File
@@ -201,6 +201,7 @@
}
.revision-description {
margin-left: 2.75rem;
padding-right: 0.5rem;
}
.author-metadata {
color: var(--color-fill-gray);
@@ -216,16 +217,18 @@
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.5rem;
min-height: 2.5rem;
padding: 0.5rem 0;
font-size: var(--font-size-small);
gap: 0.5rem;
}
.authorship-header {
display: flex;
display: inline-flex;
white-space: nowrap;
flex-wrap: wrap;
align-items: center;
padding: 0 0.5rem;
height: 1.5rem;
min-height: 1.5rem;
gap: 0.5rem;
font-size: var(--font-size-small);
}
@@ -276,6 +279,14 @@
margin-left: 1.25rem;
background-color: var(--color-fill-separator);
}
@media (max-width: 719.98px) {
.revision-box {
border-radius: 0;
}
.action {
border-radius: 0;
}
}
</style>
<div class="revision" style:margin-bottom={expanded ? "2rem" : "0.5rem"}>
@@ -289,7 +300,9 @@
</span>
</div>
<div class="revision-data">
<span title={utils.absoluteTimestamp(revisionTimestamp)}>
<span
class="global-hide-on-mobile-down"
title={utils.absoluteTimestamp(revisionTimestamp)}>
{utils.formatTimestamp(revisionTimestamp)}
</span>
{#if loading}
@@ -313,7 +326,7 @@
{/if}
<Popover
popoverPadding="0"
popoverPositionTop="2.5rem"
popoverPositionTop={expanded ? "3rem" : "2.5rem"}
popoverPositionRight="0"
popoverBorderRadius="var(--border-radius-small)">
<IconButton
@@ -419,17 +432,18 @@
{utils.formatTimestamp(revisionTimestamp)}
</span>
{#if revisionEdits.length > 1 && lastEdit}
<div class="author-metadata">•</div>
<div
class="author-metadata"
title={utils.formatEditedCaption(
lastEdit.author,
lastEdit.timestamp,
)}>
edited
• edited
</div>
{/if}
<div style="display: flex; gap: 0.5rem; margin-left: auto;">
<div
class="global-hide-on-mobile-down"
style="display: flex; gap: 0.5rem; margin-left: auto;">
{#if canEdit(revisionAuthor.id) && editRevision && revisionState === "read"}
<IconButton
title="edit revision"
@@ -470,15 +484,17 @@
<div class="actions">
{#if reactOnRevision}
{@const reactOnRevision_ = reactOnRevision}
<ReactionSelector
reactions={revisionReactions}
on:select={async ({ detail: { emoji, authors } }) => {
try {
await reactOnRevision_(authors, emoji);
} finally {
closeFocused();
}
}} />
<div class="global-hide-on-mobile-down">
<ReactionSelector
reactions={revisionReactions}
on:select={async ({ detail: { emoji, authors } }) => {
try {
await reactOnRevision_(authors, emoji);
} finally {
closeFocused();
}
}} />
</div>
{/if}
{#if revisionReactions && revisionReactions.length > 0}
<Reactions
@@ -507,7 +523,7 @@
{#if error}
<div
class="diff-error txt-monospace txt-small"
style:border-radius="var(--border-radius-small">
style:border-radius="var(--border-radius-small)">
<ErrorMessage
title="Failed to load diff for this revision"
description="Make sure you are able to connect to the seed <code>${utils.baseUrlToString(
+20
View File
@@ -0,0 +1,20 @@
<script lang="ts">
import IconSmall from "@app/components/IconSmall.svelte";
export let commentCount: number;
</script>
<style>
.comments {
color: var(--color-foreground-dim);
font-size: var(--font-size-tiny);
display: flex;
align-items: center;
gap: 0.25rem;
}
</style>
<div class="comments">
<IconSmall name="chat" />
<span>{commentCount}</span>
</div>
+6 -1
View File
@@ -39,6 +39,11 @@
white-space: pre-wrap;
margin-top: 1.5rem;
}
.button-container {
margin-left: auto;
display: flex;
gap: 0.5rem;
}
</style>
<Layout {baseUrl} {project}>
@@ -50,7 +55,7 @@
stripEmphasizedStyling
fontSize="large"
content={header.summary} />
<div style:margin-left="auto" style:display="flex" style:gap="0.5rem">
<div class="button-container">
<Link
route={{
resource: "project.source",
@@ -64,6 +64,10 @@
margin: 0.5rem 0;
font-size: var(--font-size-small);
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
<div class="teaser">
@@ -0,0 +1,48 @@
<script lang="ts">
import type { BaseUrl, Patch, Revision } from "@httpd-client";
import { HttpdClient } from "@httpd-client";
import { formatCommit } from "@app/lib/utils";
import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
import Link from "@app/components/Link.svelte";
import Loading from "@app/components/Loading.svelte";
export let projectId: string;
export let baseUrl: BaseUrl;
export let patch: Patch;
export let latestRevision: Revision;
$: diffPromise = api.project.getDiff(
projectId,
latestRevision.base,
latestRevision.oid,
);
const api = new HttpdClient(baseUrl);
</script>
{#await diffPromise}
<Loading small />
{:then { diff }}
<Link
title="Compare {formatCommit(latestRevision.base)}..{formatCommit(
latestRevision.oid,
)}"
route={{
resource: "project.patch",
project: projectId,
node: baseUrl,
patch: patch.id,
view: {
name: "diff",
fromCommit: latestRevision.base,
toCommit: latestRevision.oid,
},
}}>
<DiffStatBadge
hoverable
insertions={diff.stats.insertions}
deletions={diff.stats.deletions} />
</Link>
{/await}
+1 -1
View File
@@ -42,7 +42,7 @@
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
<Button slot="toggle" let:toggle on:click={toggle} variant="outline">
Clone
<span class="global-hide-on-small-desktop-down">Clone</span>
<Icon name="download" />
</Button>
+1 -1
View File
@@ -87,7 +87,7 @@
<style>
.more {
margin: 2rem 0;
margin-top: 2rem;
min-height: 3rem;
display: flex;
align-items: center;
+128 -119
View File
@@ -249,14 +249,17 @@
}
}
async function saveLabels(sessionId: string, labels: string[]) {
async function saveLabels(labels: string[]) {
try {
await api.project.updateIssue(
project.id,
issue.id,
{ type: "label", labels },
sessionId,
);
if (session) {
labelState = "submit";
await api.project.updateIssue(
project.id,
issue.id,
{ type: "label", labels },
session.id,
);
}
} catch (error) {
if (error instanceof Error) {
modal.show({
@@ -275,21 +278,22 @@
});
}
} finally {
labelState = "read";
await refreshIssue();
}
}
async function saveAssignees(
sessionId: string,
assignees: Reaction["authors"],
) {
async function saveAssignees(assignees: Reaction["authors"]) {
try {
await api.project.updateIssue(
project.id,
issue.id,
{ type: "assign", assignees: assignees.map(({ id }) => id) },
sessionId,
);
if (session) {
assigneeState = "submit";
await api.project.updateIssue(
project.id,
issue.id,
{ type: "assign", assignees: assignees.map(({ id }) => id) },
session.id,
);
}
} catch (error) {
if (error instanceof Error) {
modal.show({
@@ -308,6 +312,7 @@
});
}
} finally {
assigneeState = "read";
await refreshIssue();
}
}
@@ -438,7 +443,6 @@
.metadata {
display: flex;
flex-direction: column;
font-size: var(--font-size-small);
padding: 1rem;
border-left: 1px solid var(--color-border-hint);
gap: 1.5rem;
@@ -450,14 +454,6 @@
flex-direction: column;
}
.author {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 0.5rem;
font-family: var(--font-family-sans-serif);
font-size: var(--font-size-small);
}
.author-metadata {
color: var(--color-fill-gray);
font-size: var(--font-size-small);
@@ -477,10 +473,9 @@
align-items: center;
margin-left: -0.25rem;
}
@media (max-width: 720px) {
.issue {
display: block;
@media (max-width: 719.98px) {
.bottom {
padding: 0;
}
}
</style>
@@ -509,24 +504,28 @@
</div>
<div style="display: flex; gap: 0.5rem;">
{#if session && role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id) && issueState === "read"}
<Button
variant="outline"
title="edit issue"
on:click={() => (issueState = "edit")}>
<IconSmall name={"edit"} />
Edit
</Button>
<div class="global-hide-on-mobile-down">
<Button
variant="outline"
title="edit issue"
on:click={() => (issueState = "edit")}>
<IconSmall name={"edit"} />
Edit
</Button>
</div>
{/if}
{#if issueState === "read"}
<Share {baseUrl} />
{#if session && role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id)}
<CobStateButton
items={items.filter(
([, state]) => !isEqual(state, issue.state),
)}
{selectedItem}
state={issue.state}
save={partial(saveStatus, session.id)} />
<div class="global-hide-on-small-desktop-down">
<CobStateButton
items={items.filter(
([, state]) => !isEqual(state, issue.state),
)}
{selectedItem}
state={issue.state}
save={partial(saveStatus, session.id)} />
</div>
{/if}
{/if}
</div>
@@ -544,7 +543,55 @@
{issue.state.reason}
</Badge>
{/if}
<NodeId
stylePopoverPositionLeft="-13px"
nodeId={issue.author.id}
alias={issue.author.alias} />
opened
<CopyableId id={issue.id} style="oid">
{utils.formatObjectId(issue.id)}
</CopyableId>
<span title={utils.absoluteTimestamp(issue.discussion[0].timestamp)}>
{utils.formatTimestamp(issue.discussion[0].timestamp)}
</span>
{#if lastDescriptionEdit}
<div
class="author-metadata"
title={utils.formatEditedCaption(
lastDescriptionEdit.author,
lastDescriptionEdit.timestamp,
)}>
• edited
</div>
{/if}
</svelte:fragment>
<div slot="subtitle" class="global-hide-on-desktop-up">
<div
style:margin-top="2rem"
style="display: flex; flex-direction: column; gap: 0.5rem;">
<AssigneeInput
locallyAuthenticated={role.isDelegate(
session?.publicKey,
delegates,
)}
assignees={issue.assignees}
submitInProgress={assigneeState === "submit"}
on:save={({ detail: newAssignees }) => {
void saveAssignees(newAssignees);
}} />
<LabelInput
locallyAuthenticated={role.isDelegate(
session?.publicKey,
delegates,
)}
labels={issue.labels}
submitInProgress={labelState === "submit"}
on:save={({ detail: newLabels }) => void saveLabels(newLabels)} />
<div class="global-hide-on-mobile-down">
<Embeds embeds={uniqueEmbeds} />
</div>
</div>
</div>
<svelte:fragment slot="description">
{#if $experimental && issueState !== "read"}
<ExtendedTextarea
@@ -589,17 +636,19 @@
{/if}
<div class="reactions">
{#if $experimental && session}
<ReactionSelector
reactions={issue.discussion[0].reactions}
on:select={async ({ detail: { authors, emoji } }) => {
try {
if (session) {
await reactOnComment(session, issue.id, authors, emoji);
<div class="global-hide-on-mobile-down">
<ReactionSelector
reactions={issue.discussion[0].reactions}
on:select={async ({ detail: { authors, emoji } }) => {
try {
if (session) {
await reactOnComment(session, issue.id, authors, emoji);
}
} finally {
closeFocused();
}
} finally {
closeFocused();
}
}} />
}} />
</div>
{/if}
{#if issue.discussion[0].reactions.length > 0}
<Reactions
@@ -609,30 +658,6 @@
{/if}
</div>
</svelte:fragment>
<div class="author" slot="author">
<NodeId
stylePopoverPositionLeft="-13px"
nodeId={issue.author.id}
alias={issue.author.alias} />
opened
<CopyableId id={issue.id} style="oid">
{utils.formatObjectId(issue.id)}
</CopyableId>
<span title={utils.absoluteTimestamp(issue.discussion[0].timestamp)}>
{utils.formatTimestamp(issue.discussion[0].timestamp)}
</span>
{#if lastDescriptionEdit}
<div class="author-metadata">•</div>
<div
class="author-metadata"
title={utils.formatEditedCaption(
lastDescriptionEdit.author,
lastDescriptionEdit.timestamp,
)}>
edited
</div>
{/if}
</div>
</CobHeader>
<div class="bottom">
{#if threads.length > 0}
@@ -664,46 +689,40 @@
</div>
{/if}
{#if $experimental && session}
<div class="connector" />
<CommentToggleInput
focus
rawPath={rawPath(project.head)}
placeholder="Leave your comment"
enableAttachments
submit={partial(createComment, session.id)} />
<div
style="display:flex; flex-direction: column; align-items: flex-start;">
{#if role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id)}
<div class="connector" />
<CobStateButton
items={items.filter(
([, state]) => !isEqual(state, issue.state),
)}
{selectedItem}
state={issue.state}
save={partial(saveStatus, session.id)} />
{/if}
<div class="global-hide-on-mobile-down">
<div class="connector" />
<CommentToggleInput
focus
rawPath={rawPath(project.head)}
placeholder="Leave your comment"
enableAttachments
submit={partial(createComment, session.id)} />
<div
style="display:flex; flex-direction: column; align-items: flex-start;">
{#if role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id)}
<div class="connector" />
<CobStateButton
items={items.filter(
([, state]) => !isEqual(state, issue.state),
)}
{selectedItem}
state={issue.state}
save={partial(saveStatus, session.id)} />
{/if}
</div>
</div>
{/if}
</div>
</div>
<div class="metadata global-hide-on-mobile">
<div class="metadata global-hide-on-medium-desktop-down">
<AssigneeInput
locallyAuthenticated={Boolean(
role.isDelegate(session?.publicKey, delegates),
)}
assignees={issue.assignees}
submitInProgress={assigneeState === "submit"}
on:save={async ({ detail: newAssignees }) => {
if (session) {
assigneeState = "submit";
try {
await saveAssignees(session.id, newAssignees);
} finally {
assigneeState = "read";
}
}
await refreshIssue();
on:save={({ detail: newAssignees }) => {
void saveAssignees(newAssignees);
}} />
<LabelInput
locallyAuthenticated={Boolean(
@@ -711,17 +730,7 @@
)}
labels={issue.labels}
submitInProgress={labelState === "submit"}
on:save={async ({ detail: newLabels }) => {
if (session) {
labelState = "submit";
try {
await saveLabels(session.id, newLabels);
} finally {
labelState = "read";
}
}
await refreshIssue();
}} />
on:save={({ detail: newLabels }) => void saveLabels(newLabels)} />
<Embeds embeds={uniqueEmbeds} />
</div>
</div>
+15 -46
View File
@@ -7,12 +7,14 @@
formatTimestamp,
} from "@app/lib/utils";
import Badge from "@app/components/Badge.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
import Link from "@app/components/Link.svelte";
import NodeId from "@app/components/NodeId.svelte";
import CommentCounter from "../CommentCounter.svelte";
import Labels from "../Cob/Labels.svelte";
export let baseUrl: BaseUrl;
export let issue: Issue;
export let projectId: string;
@@ -23,8 +25,6 @@
}
return acc;
}, 0);
let hover = false;
</script>
<style>
@@ -57,25 +57,11 @@
.issue-title:hover {
text-decoration: underline;
}
.comment-count {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
height: 1.5rem;
}
.labels {
display: flex;
flex-direction: row;
gap: 0.5rem;
flex-wrap: wrap;
}
.right {
display: flex;
gap: 1rem;
margin-left: auto;
color: var(--color-foreground-dim);
font-size: var(--font-size-tiny);
min-height: 1.5rem;
align-items: center;
}
.state {
justify-self: center;
@@ -91,12 +77,7 @@
}
</style>
<div
role="button"
tabindex="0"
class="issue-teaser"
on:mouseenter={() => (hover = true)}
on:mouseleave={() => (hover = false)}>
<div role="button" tabindex="0" class="issue-teaser">
<div
class="state"
class:closed={issue.state.status === "closed"}
@@ -116,32 +97,20 @@
{#if !issue.title}
<span class="txt-missing">No title</span>
{:else}
<InlineMarkdown fontSize="regular" content={issue.title} />
<InlineMarkdown fontSize="regular" content={issue.title}>
{#if issue.labels.length > 0}
<span
style="display: inline-flex; gap: 0.5rem; flex-wrap: wrap;">
<Labels labels={issue.labels} />
</span>
{/if}
</InlineMarkdown>
{/if}
</span>
</Link>
<span class="labels">
{#each issue.labels.slice(0, 4) as label}
<Badge variant={hover ? "background" : "neutral"}>
{label}
</Badge>
{/each}
{#if issue.labels.length > 4}
<Badge
title={issue.labels.slice(4, undefined).join(" ")}
variant={hover ? "background" : "neutral"}>
+{issue.labels.length - 4} more labels
</Badge>
{/if}
</span>
<div class="right">
{#if commentCount > 0}
<div style:display="flex" style:gap="1rem">
<div class="comment-count">
<IconSmall name="chat" />
<span>{commentCount}</span>
</div>
</div>
<CommentCounter {commentCount} />
{/if}
</div>
</div>
+14 -12
View File
@@ -69,7 +69,7 @@
<style>
.more {
margin: 2rem 0;
margin-top: 2rem;
min-height: 3rem;
display: flex;
align-items: center;
@@ -148,17 +148,19 @@
<div style="margin-left: auto; display: flex; gap: 1rem;">
<Share {baseUrl} />
{#if $experimental && $httpdStore.state === "authenticated" && isLocal(baseUrl.hostname)}
<Link
route={{
resource: "project.newIssue",
project: project.id,
node: baseUrl,
}}>
<Button variant="secondary">
<IconSmall name="plus" />
New Issue
</Button>
</Link>
<div class="global-hide-on-mobile-down">
<Link
route={{
resource: "project.newIssue",
project: project.id,
node: baseUrl,
}}>
<Button variant="secondary">
<IconSmall name="plus" />
New Issue
</Button>
</Link>
</div>
{/if}
</div>
</div>
+9 -7
View File
@@ -13,6 +13,7 @@
export let activeTab: ActiveTab | undefined = undefined;
export let baseUrl: BaseUrl;
export let project: Project;
export let stylePaddingBottom: string = "2.5rem";
</script>
<style>
@@ -41,13 +42,10 @@
display: none;
}
@media (max-width: 720px) {
@media (max-width: 719.98px) {
.desktop-header {
display: none;
}
.sidebar {
display: none;
}
.content {
overflow-y: scroll;
overflow-x: hidden;
@@ -66,11 +64,15 @@
<AppHeader />
</div>
<div class="sidebar">
<div class="sidebar global-hide-on-medium-desktop-down">
<Sidebar {activeTab} {baseUrl} {project} />
</div>
<div class="content">
<div class="sidebar global-hide-on-mobile-down global-hide-on-desktop-up">
<Sidebar {activeTab} {baseUrl} {project} collapsedOnly />
</div>
<div class="content" style:padding-bottom={stylePaddingBottom}>
<slot name="header" />
<slot name="subheader" />
<slot />
@@ -90,7 +92,7 @@
<Button
variant={activeTab === "source" ? "secondary" : "secondary-mobile"}
styleWidth="100%">
<IconSmall name="home" />
<IconSmall name="chevron-left-right" />
</Button>
</Link>
</div>
+144 -208
View File
@@ -34,6 +34,10 @@
}
export type Timeline = TimelineMerge | TimelineReview | TimelineThread;
export type PatchReviews = Record<
string,
{ latest: boolean; review: Review }
>;
</script>
<script lang="ts">
@@ -64,8 +68,6 @@
import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
import CopyableId from "@app/components/CopyableId.svelte";
import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
import DropdownList from "@app/components/DropdownList.svelte";
import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
import Embeds from "@app/views/projects/Cob/Embeds.svelte";
import ErrorModal from "@app/modals/ErrorModal.svelte";
import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
@@ -77,13 +79,15 @@
import Markdown from "@app/components/Markdown.svelte";
import NodeId from "@app/components/NodeId.svelte";
import Placeholder from "@app/components/Placeholder.svelte";
import Popover, { closeFocused } from "@app/components/Popover.svelte";
import Radio from "@app/components/Radio.svelte";
import ReactionSelector from "@app/components/ReactionSelector.svelte";
import Reactions from "@app/components/Reactions.svelte";
import Reviews from "@app/views/projects/Cob/Reviews.svelte";
import RevisionComponent from "@app/views/projects/Cob/Revision.svelte";
import RevisionSelector from "@app/views/projects/Patch/RevisionSelector.svelte";
import Share from "@app/views/projects/Share.svelte";
import TextInput from "@app/components/TextInput.svelte";
import { closeFocused } from "@app/components/Popover.svelte";
export let baseUrl: BaseUrl;
export let patch: Patch;
@@ -411,14 +415,17 @@
}
}
async function saveLabels(sessionId: string, labels: string[]) {
async function saveLabels(labels: string[]) {
try {
await api.project.updatePatch(
project.id,
patch.id,
{ type: "label", labels },
sessionId,
);
if (session) {
labelState = "submit";
await api.project.updatePatch(
project.id,
patch.id,
{ type: "label", labels },
session.id,
);
}
} catch (error) {
if (error instanceof Error) {
modal.show({
@@ -437,6 +444,7 @@
});
}
} finally {
labelState = "read";
await refreshPatch();
}
}
@@ -550,7 +558,7 @@
$: description = patch.revisions[0].description;
$: lastEdit = patch.revisions[0].edits.at(-1);
$: newDescription = description;
$: patchReviews = computeReviews(patch);
$: reviews = computeReviews(patch);
$: selectedItem = patch.state.status === "open" ? items[1] : items[0];
$: timelineTuple = patch.revisions.map<
[
@@ -646,7 +654,7 @@
}
.bottom {
background-color: var(--color-background-default);
padding: 1rem;
padding: 1rem 1rem 0 1rem;
height: 100%;
}
.actions {
@@ -669,39 +677,10 @@
width: 1rem;
height: 100%;
}
.author {
display: flex;
align-items: center;
flex-wrap: nowrap;
font-family: var(--font-family-sans-serif);
font-size: var(--font-size-small);
gap: 0.5rem;
}
.metadata-section-header {
font-size: var(--font-size-small);
margin-bottom: 0.75rem;
}
.metadata-section-body {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.author-metadata {
color: var(--color-fill-gray);
font-size: var(--font-size-small);
}
.review {
color: var(--color-fill-gray);
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.review-accept {
color: var(--color-foreground-success);
}
.review-reject {
color: var(--color-foreground-red);
}
.revision-description {
display: flex;
flex-direction: column;
@@ -718,10 +697,13 @@
margin-left: 1.25rem;
background-color: var(--color-fill-separator);
}
@media (max-width: 720px) {
@media (max-width: 719.98px) {
.patch {
display: block;
}
.bottom {
padding: 1rem 0 0 0;
}
}
</style>
@@ -749,24 +731,28 @@
{/if}
</div>
{#if session && role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id) && patchState === "read"}
<Button
variant="outline"
title="edit patch"
on:click={() => (patchState = "edit")}>
<IconSmall name={"edit"} />
Edit
</Button>
<div class="global-hide-on-mobile-down">
<Button
variant="outline"
title="edit patch"
on:click={() => (patchState = "edit")}>
<IconSmall name={"edit"} />
Edit
</Button>
</div>
{/if}
{#if patchState === "read"}
<Share {baseUrl} />
{#if session && role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id)}
<CobStateButton
items={items.filter(
([, state]) => !isEqual(state, patch.state),
)}
{selectedItem}
state={patch.state}
save={partial(saveStatus, session.id)} />
<div class="global-hide-on-small-desktop-down">
<CobStateButton
items={items.filter(
([, state]) => !isEqual(state, patch.state),
)}
{selectedItem}
state={patch.state}
save={partial(saveStatus, session.id)} />
</div>
{/if}
{/if}
</svelte:fragment>
@@ -788,7 +774,46 @@
insertions={stats.insertions}
deletions={stats.deletions} />
</Link>
<NodeId
stylePopoverPositionLeft="-13px"
nodeId={patch.author.id}
alias={patch.author.alias} />
opened
<CopyableId id={patch.id} style="oid">
{utils.formatObjectId(patch.id)}
</CopyableId>
<span title={utils.absoluteTimestamp(patch.revisions[0].timestamp)}>
{utils.formatTimestamp(patch.revisions[0].timestamp)}
</span>
{#if patch.revisions[0].edits.length > 1 && lastEdit}
<div
class="author-metadata"
title={utils.formatEditedCaption(
lastEdit.author,
lastEdit.timestamp,
)}>
• edited
</div>
{/if}
</svelte:fragment>
<div slot="subtitle" class="global-hide-on-desktop-up">
<div
style:margin-top="2rem"
style="display: flex; flex-direction: column; gap: 0.5rem;">
<Reviews {reviews} />
<LabelInput
locallyAuthenticated={role.isDelegate(
session?.publicKey,
delegates,
)}
submitInProgress={labelState === "submit"}
labels={patch.labels}
on:save={({ detail: newLabels }) => {
void saveLabels(newLabels);
}} />
<Embeds embeds={uniqueEmbeds} />
</div>
</div>
<svelte:fragment slot="description">
<div class="revision-description">
{#if $experimental && session && patchState !== "read" && lastEdit}
@@ -832,22 +857,24 @@
{#if ($experimental && session) || (firstRevision.revisionReactions && firstRevision.revisionReactions.length > 0)}
<div class="actions">
{#if session}
<ReactionSelector
reactions={firstRevision.revisionReactions}
on:select={async ({ detail: { emoji, authors } }) => {
if (session) {
try {
await reactOnRevision(
session,
patch.id,
authors,
emoji,
);
} finally {
closeFocused();
<div class="global-hide-on-mobile-down">
<ReactionSelector
reactions={firstRevision.revisionReactions}
on:select={async ({ detail: { emoji, authors } }) => {
if (session) {
try {
await reactOnRevision(
session,
patch.id,
authors,
emoji,
);
} finally {
closeFocused();
}
}
}
}} />
}} />
</div>
{/if}
{#if firstRevision.revisionReactions.length > 0}
<Reactions
@@ -859,30 +886,6 @@
{/if}
</div>
</svelte:fragment>
<div class="author" slot="author">
<NodeId
stylePopoverPositionLeft="-13px"
nodeId={patch.author.id}
alias={patch.author.alias} />
opened
<CopyableId id={patch.id} style="oid">
{utils.formatObjectId(patch.id)}
</CopyableId>
<span title={utils.absoluteTimestamp(patch.revisions[0].timestamp)}>
{utils.formatTimestamp(patch.revisions[0].timestamp)}
</span>
{#if patch.revisions[0].edits.length > 1 && lastEdit}
<div class="author-metadata">•</div>
<div
class="author-metadata"
title={utils.formatEditedCaption(
lastEdit.author,
lastEdit.timestamp,
)}>
edited
</div>
{/if}
</div>
</CobHeader>
<div class="tabs">
@@ -923,67 +926,27 @@
{/if}
</Radio>
<div style="margin-left: auto; margin-top: -0.5rem;">
{#if view.name === "changes"}
<div style="margin-left: auto; ">
<Popover
popoverPadding="0"
popoverPositionTop="2.5rem"
popoverBorderRadius="var(--border-radius-small)">
<Button
let:expanded
slot="toggle"
let:toggle
on:click={toggle}
size="regular"
disabled={patch.revisions.length === 1}>
<span style:color="var(--color-foreground-contrast)">
Revision
</span>
<span
style:color="var(--color-fill-secondary)"
style:font-family="var(--font-family-monospace)">
{utils.formatObjectId(view.revision)}
</span>
<IconSmall name={expanded ? "chevron-up" : "chevron-down"} />
</Button>
<DropdownList slot="popover" items={patch.revisions}>
<svelte:fragment slot="item" let:item>
<Link
on:afterNavigate={closeFocused}
route={{
resource: "project.patch",
project: project.id,
node: baseUrl,
patch: patch.id,
view: {
name: view.name,
revision: item.id,
},
}}>
<DropdownListItem selected={item.id === view.revision}>
<span
style:color={item.id === view.revision
? "var(--color-foreground-contrast)"
: "var(--color-fill-gray)"}>
Revision
</span>
<span
style:color="var(--color-fill-secondary)"
style:font-family="var(--font-family-monospace)">
{utils.formatObjectId(item.id)}
</span>
</DropdownListItem>
</Link>
</svelte:fragment>
</DropdownList>
</Popover>
</div>
{/if}
</div>
{#if view.name === "changes"}
<div
class="global-hide-on-mobile-down"
style="margin-left: auto; margin-top: -0.5rem;">
<RevisionSelector {view} {baseUrl} {patch} {project} />
</div>
{/if}
<div class="tabs-spacer" />
</div>
<div class="bottom">
{#if view.name === "changes"}
<div
style:width="100%"
style:padding="0 1rem"
style:display="flex"
class="global-hide-on-small-desktop-up">
<div style:margin-left="auto">
<RevisionSelector {view} {baseUrl} {patch} {project} />
</div>
</div>
{/if}
{#if view.name === "diff"}
<Changeset
{baseUrl}
@@ -1030,29 +993,31 @@
previousRevOid={previousRevision?.oid}>
{#if index === patch.revisions.length - 1}
{#if $experimental && session && view.name === "activity"}
<div class="connector" />
<CommentToggleInput
rawPath={rawPath(patch.revisions[0].id)}
focus
enableAttachments
placeholder="Leave your comment"
submit={partial(
createComment,
session.id,
revision.revisionId,
)} />
{#if role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id)}
<div class="global-hide-on-mobile-down">
<div class="connector" />
<div style="display: flex;">
<CobStateButton
items={items.filter(
([, state]) => !isEqual(state, patch.state),
)}
{selectedItem}
state={patch.state}
save={partial(saveStatus, session.id)} />
</div>
{/if}
<CommentToggleInput
rawPath={rawPath(patch.revisions[0].id)}
focus
enableAttachments
placeholder="Leave your comment"
submit={partial(
createComment,
session.id,
revision.revisionId,
)} />
{#if role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id)}
<div class="connector" />
<div style="display: flex;">
<CobStateButton
items={items.filter(
([, state]) => !isEqual(state, patch.state),
)}
{selectedItem}
state={patch.state}
save={partial(saveStatus, session.id)} />
</div>
{/if}
</div>
{/if}
{/if}
</RevisionComponent>
@@ -1076,43 +1041,14 @@
</div>
</div>
<div class="metadata global-hide-on-mobile">
<div>
<div class="metadata-section-header">Reviews</div>
<div class="metadata-section-body">
{#each Object.values(patchReviews) as { latest, review }}
<div class="review" class:txt-missing={!latest}>
<span
class:review-accept={review.verdict === "accept"}
class:review-reject={review.verdict === "reject"}>
{#if review.verdict === "accept"}
<IconSmall name="checkmark" />
{:else if review.verdict === "reject"}
<IconSmall name="cross" />
{:else}
<IconSmall name="chat" />
{/if}
</span>
<NodeId nodeId={review.author.id} alias={review.author.alias} />
</div>
{:else}
<div class="txt-missing">No reviews</div>
{/each}
</div>
</div>
<div class="metadata global-hide-on-medium-desktop-down">
<Reviews {reviews} />
<LabelInput
locallyAuthenticated={role.isDelegate(session?.publicKey, delegates)}
submitInProgress={labelState === "submit"}
labels={patch.labels}
on:save={async ({ detail: newLabels }) => {
if (session) {
labelState = "submit";
try {
await saveLabels(session.id, newLabels);
} finally {
labelState = "read";
}
}
on:save={({ detail: newLabels }) => {
void saveLabels(newLabels);
}} />
<Embeds embeds={uniqueEmbeds} />
</div>
+33 -86
View File
@@ -2,42 +2,32 @@
import type { BaseUrl } from "@httpd-client";
import type { Patch } from "@httpd-client";
import { HttpdClient } from "@httpd-client";
import {
absoluteTimestamp,
formatObjectId,
formatTimestamp,
formatCommit,
} from "@app/lib/utils";
import Badge from "@app/components/Badge.svelte";
import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
import Link from "@app/components/Link.svelte";
import NodeId from "@app/components/NodeId.svelte";
import Loading from "@app/components/Loading.svelte";
import Labels from "../Cob/Labels.svelte";
import DiffStatBadgeLoader from "../DiffStatBadgeLoader.svelte";
import CommentCounter from "../CommentCounter.svelte";
export let projectId: string;
export let baseUrl: BaseUrl;
export let patch: Patch;
const api = new HttpdClient(baseUrl);
const latestRevisionIndex = patch.revisions.length - 1;
const latestRevision = patch.revisions[latestRevisionIndex];
$: diffPromise = api.project.getDiff(
projectId,
latestRevision.base,
latestRevision.oid,
);
// Counts the amount of comments in all the discussions over all revisions.
$: commentCount = patch.revisions.reduce(
(acc, curr) => acc + curr.discussions.reduce(acc => acc + 1, 0),
0,
);
let hover = false;
</script>
<style>
@@ -71,10 +61,9 @@
text-decoration: underline;
}
.right {
margin-left: auto;
display: flex;
align-items: flex-start;
gap: 1rem;
margin-left: auto;
}
.state {
justify-self: center;
@@ -82,22 +71,6 @@
margin-right: 0.5rem;
padding: 0.25rem 0;
}
.labels {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.comments {
color: var(--color-foreground-dim);
font-size: var(--font-size-tiny);
display: flex;
align-items: center;
gap: 0.25rem;
}
.label {
overflow: hidden;
text-overflow: ellipsis;
}
.draft {
color: var(--color-foreground-dim);
}
@@ -110,14 +83,22 @@
.merged {
color: var(--color-fill-primary);
}
.diff-comment {
display: flex;
flex-direction: row;
gap: 0.5rem;
margin-left: 0.5rem;
min-height: 1.5rem;
}
@media (max-width: 719.98px) {
.diff-comment {
flex-direction: column-reverse;
align-items: flex-end;
}
}
</style>
<div
role="button"
tabindex="0"
class="patch-teaser"
on:mouseenter={() => (hover = true)}
on:mouseleave={() => (hover = false)}>
<div role="button" tabindex="0" class="patch-teaser">
<div
class="state"
class:draft={patch.state.status === "draft"}
@@ -136,25 +117,15 @@
patch: patch.id,
}}>
<span class="patch-title">
<InlineMarkdown fontSize="regular" content={patch.title} />
<InlineMarkdown fontSize="regular" content={patch.title}>
{#if patch.labels.length > 0}
<span style="display: inline-flex; gap: 0.5rem; flex-wrap: wrap;">
<Labels labels={patch.labels} />
</span>
{/if}
</InlineMarkdown>
</span>
</Link>
<span class="labels">
{#each patch.labels.slice(0, 4) as label}
<Badge
style="max-width:7rem"
variant={hover ? "background" : "neutral"}>
<span class="label">{label}</span>
</Badge>
{/each}
{#if patch.labels.length > 4}
<Badge
title={patch.labels.slice(4, undefined).join(" ")}
variant={hover ? "background" : "neutral"}>
<span class="label">+{patch.labels.length - 4} more labels</span>
</Badge>
{/if}
</span>
</div>
<div class="summary">
<span class="subtitle">
@@ -165,8 +136,10 @@
{patch.revisions.length > 1 ? "updated" : "opened"}
<span class="global-oid">{formatObjectId(patch.id)}</span>
{#if patch.revisions.length > 1}
to <span class="global-oid">
{formatObjectId(patch.revisions[patch.revisions.length - 1].id)}
<span class="global-hide-on-mobile-down">
to <span class="global-oid">
{formatObjectId(patch.revisions[patch.revisions.length - 1].id)}
</span>
</span>
{/if}
<span title={absoluteTimestamp(latestRevision.timestamp)}>
@@ -176,37 +149,11 @@
</div>
</div>
<div class="right">
<div style:display="flex" style:gap="1rem">
<div class="diff-comment">
{#if commentCount > 0}
<div class="comments">
<IconSmall name="chat" />
<span>{commentCount}</span>
</div>
<CommentCounter {commentCount} />
{/if}
{#await diffPromise}
<Loading small />
{:then { diff }}
<Link
title="Compare {formatCommit(latestRevision.base)}..{formatCommit(
latestRevision.oid,
)}"
route={{
resource: "project.patch",
project: projectId,
node: baseUrl,
patch: patch.id,
view: {
name: "diff",
fromCommit: latestRevision.base,
toCommit: latestRevision.oid,
},
}}>
<DiffStatBadge
hoverable
insertions={diff.stats.insertions}
deletions={diff.stats.deletions} />
</Link>
{/await}
<DiffStatBadgeLoader {projectId} {baseUrl} {patch} {latestRevision} />
</div>
</div>
</div>
@@ -0,0 +1,69 @@
<script lang="ts">
import type { PatchView } from "../router";
import type { BaseUrl, Patch, Project } from "@httpd-client";
import * as utils 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 IconSmall from "@app/components/IconSmall.svelte";
import Link from "@app/components/Link.svelte";
import Popover from "@app/components/Popover.svelte";
import { closeFocused } from "@app/components/Popover.svelte";
export let view: Extract<PatchView, { name: "changes" }>;
export let baseUrl: BaseUrl;
export let patch: Patch;
export let project: Project;
</script>
<Popover
popoverPadding="0"
popoverPositionTop="3rem"
popoverBorderRadius="var(--border-radius-small)">
<Button
let:expanded
slot="toggle"
let:toggle
on:click={toggle}
size="regular"
disabled={patch.revisions.length === 1}>
<span style:color="var(--color-foreground-contrast)">Revision</span>
<span
style:color="var(--color-fill-secondary)"
style:font-family="var(--font-family-monospace)">
{utils.formatObjectId(view.revision)}
</span>
<IconSmall name={expanded ? "chevron-up" : "chevron-down"} />
</Button>
<DropdownList slot="popover" items={patch.revisions}>
<svelte:fragment slot="item" let:item>
<Link
on:afterNavigate={closeFocused}
route={{
resource: "project.patch",
project: project.id,
node: baseUrl,
patch: patch.id,
view: {
name: view.name,
revision: item.id,
},
}}>
<DropdownListItem selected={item.id === view.revision}>
<span
style:color={item.id === view.revision
? "var(--color-foreground-contrast)"
: "var(--color-fill-gray)"}>
Revision
</span>
<span
style:color="var(--color-fill-secondary)"
style:font-family="var(--font-family-monospace)">
{utils.formatObjectId(item.id)}
</span>
</DropdownListItem>
</Link>
</svelte:fragment>
</DropdownList>
</Popover>
+18 -16
View File
@@ -78,7 +78,7 @@
<style>
.more {
margin: 2rem 0;
margin-top: 2rem;
min-height: 3rem;
display: flex;
align-items: center;
@@ -162,22 +162,24 @@
<div style="margin-left: auto; display: flex; gap: 1rem;">
<Share {baseUrl} />
{#if $experimental && $httpdStore.state === "authenticated" && isLocal(baseUrl.hostname)}
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
<Button
slot="toggle"
let:toggle
on:click={toggle}
variant="secondary">
<IconSmall name="plus" />
New Patch
</Button>
<div class="global-hide-on-mobile-down">
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
<Button
slot="toggle"
let:toggle
on:click={toggle}
variant="secondary">
<IconSmall name="plus" />
New Patch
</Button>
<div slot="popover" class="popover txt-small">
To create a patch, first checkout a new branch and commit your
changes, then run the following command.
<Command command="git push rad HEAD:refs/patches" />
</div>
</Popover>
<div slot="popover" class="popover txt-small">
To create a patch, first checkout a new branch and commit your
changes, then run the following command.
<Command command="git push rad HEAD:refs/patches" />
</div>
</Popover>
</div>
{/if}
</div>
</div>
+4 -2
View File
@@ -54,13 +54,15 @@
let:toggle
on:click={toggle}>
<IconSmall name="link" />
{caption}
<span class="global-hide-on-small-desktop-down">
{caption}
</span>
</Button>
<ShareButton slot="popover" />
</Popover>
{:else}
<Button variant="outline" size="regular" on:click={copy}>
<IconSmall name={shareIcon} />
Copy link
<span class="global-hide-on-small-desktop-down">Copy link</span>
</Button>
{/if}
+40 -34
View File
@@ -23,8 +23,9 @@
export let activeTab: ActiveTab | undefined = undefined;
export let baseUrl: BaseUrl;
export let project: Project;
export let collapsedOnly = false;
let expanded = loadSidebarState();
let expanded = collapsedOnly ? false : loadSidebarState();
export function storeSidebarState(expanded: boolean): void {
window.localStorage.setItem(
@@ -338,40 +339,45 @@
{/if}
{/if}
</div>
<div class="sidebar-footer" style:flex-direction="row">
<Button title={"Collapse"} on:click={toggleSidebar} variant="background">
<div class="icon" class:expanded>
<IconSmall name="chevron-left" />
</div>
</Button>
<div style:width="1.5rem" />
<div class="horizontal-buttons" class:expanded>
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="background"
title="Settings"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="settings" />
Settings
</Button>
{#if !collapsedOnly}
<div class="sidebar-footer" style:flex-direction="row">
<Button
title={"Collapse"}
on:click={toggleSidebar}
variant="background">
<div class="icon" class:expanded>
<IconSmall name="chevron-left" />
</div>
</Button>
<div style:width="1.5rem" />
<div class="horizontal-buttons" class:expanded>
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="background"
title="Settings"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="settings" />
Settings
</Button>
<Settings slot="popover" />
</Popover>
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="background"
title="Help"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="help" />
Help
</Button>
<Help slot="popover" />
</Popover>
<Settings slot="popover" />
</Popover>
<Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
<Button
variant="background"
title="Help"
slot="toggle"
let:toggle
on:click={toggle}>
<IconSmall name="help" />
Help
</Button>
<Help slot="popover" />
</Popover>
</div>
</div>
</div>
{/if}
</div>
</div>
+11 -3
View File
@@ -99,6 +99,7 @@
display: flex;
flex-direction: column;
width: 100%;
padding-bottom: 2.5rem;
/* To allow pre elements to shrink when overflowing */
min-width: 0;
}
@@ -119,9 +120,16 @@
top: 0rem;
max-height: calc(100vh - 5.5rem);
}
@media (max-width: 719.98px) {
.container {
display: flex;
width: inherit;
padding: 0;
}
}
</style>
<Layout {baseUrl} {project} activeTab="source">
<Layout {baseUrl} {project} activeTab="source" stylePaddingBottom="0">
<ProjectNameHeader {project} {baseUrl} {seeding} slot="header" />
<div style:margin="1rem 0 1rem 1rem" slot="subheader">
@@ -136,7 +144,7 @@
filesLinkActive={true}
historyLinkActive={false} />
</div>
<div class="global-hide-on-desktop">
<div class="global-hide-on-medium-desktop-up">
{#if tree.entries.length > 0}
<div style:margin="1rem">
<Button
@@ -170,7 +178,7 @@
<div class="container center-content">
{#if tree.entries.length > 0}
<div class="column-left global-hide-on-mobile">
<div class="column-left global-hide-on-small-desktop-down">
<div class="source-tree sticky">
<TreeComponent
projectId={project.id}
+10 -2
View File
@@ -137,13 +137,21 @@
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.markdown-wrapper {
padding: 2rem;
}
@media (max-width: 719.98px) {
.markdown-wrapper {
padding: 1rem;
}
}
</style>
<File sticky={false}>
<FilePath slot="left-header" filenameWithPath={blob.path} />
<svelte:fragment slot="right-header">
<CommitButton styleRoundBorders {projectId} {baseUrl} commit={lastCommit} />
<div class="global-hide-on-mobile teaser-buttons">
<div class="global-hide-on-mobile-down teaser-buttons">
{#if enablePreview}
<Radio ariaLabel="Toggle render method">
<Button
@@ -188,7 +196,7 @@
{/if}
{:else if preview && blob.content}
{#if isMarkdown}
<div style:padding="2rem">
<div class="markdown-wrapper">
<Markdown content={blob.content} {rawPath} {path} />
</div>
{:else if isSvg}
@@ -75,15 +75,18 @@
Private
</Badge>
{/if}
<div
class="global-hide-on-mobile"
style="margin-left: auto; display: flex; gap: 0.5rem;">
<div style="margin-left: auto; display: flex; gap: 0.5rem;">
<Share {baseUrl} />
<CloneButton {baseUrl} id={project.id} name={project.name} />
<SeedButton
{seeding}
seedCount={project.seeding}
projectId={project.id} />
<div
style:display="flex"
style:gap="0.5rem"
class="global-hide-on-mobile-down">
<CloneButton {baseUrl} id={project.id} name={project.name} />
<SeedButton
{seeding}
seedCount={project.seeding}
projectId={project.id} />
</div>
</div>
</div>
<div class="id">
@@ -43,7 +43,9 @@
<div class="identifier global-commit">
{commitShortId}
</div>
<span>{commit.summary}</span>
<span class="global-hide-on-small-desktop-down">
{commit.summary}
</span>
</div>
</Button>
</Link>
+1 -1
View File
@@ -397,7 +397,7 @@ test("only one modal can be open at a time", async ({ page }) => {
await expect(page.getByText("bob")).not.toBeVisible();
await expect(page.getByText("feature/branch")).not.toBeVisible();
await page.getByText("Settings").click();
await page.getByRole("button", { name: "Settings" }).click();
await expect(page.getByText("Code font")).toBeVisible();
await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
await expect(page.getByText("bob")).not.toBeVisible();
+3 -3
View File
@@ -180,7 +180,7 @@ test("handling embeds", async ({ page, authenticatedPeer }) => {
`${scheme}://${hostname}:${port}/raw/rad:z2J7s48EbCBckcEmj2dm5eaFVoBsy/blobs/bae036309c2182c7304c97956969369823b5c6ad?mime=image/png`,
);
await expect(
page.locator(".badge").filter({ hasText: "radicle-228x228.png" }),
page.getByRole("button", { name: "radicle-228x228.png" }),
).toBeVisible();
await page.getByRole("button", { name: "Edit" }).click();
@@ -201,9 +201,9 @@ test("handling embeds", async ({ page, authenticatedPeer }) => {
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("button", { name: "Submit" })).toBeHidden();
await expect(
page.locator(".badge").filter({ hasText: "apple-touch-icon.png" }),
page.getByRole("button", { name: "apple-touch-icon.png" }),
).toBeVisible();
await expect(
page.locator(".badge").filter({ hasText: "radicle-228x228.png" }),
page.getByRole("button", { name: "radicle-228x228.png" }),
).toBeVisible();
});
+3 -3
View File
@@ -88,12 +88,12 @@ test("create a new issue", async ({ page, authenticatedPeer }) => {
await expect(page.getByText("This is a title")).toBeVisible();
await expect(page.getByText("This is a description")).toBeVisible();
await expect(
page.getByText(
`did:key:${authenticatedPeer.nodeId.substring(
page.getByRole("button", {
name: `did:key:${authenticatedPeer.nodeId.substring(
0,
6,
)}…${authenticatedPeer.nodeId.slice(-6)}`,
),
}),
).toBeVisible();
await expect(
page.getByRole("button", { name: "documentation" }),
+3 -3
View File
@@ -134,7 +134,7 @@ test("handling embeds", async ({ page, authenticatedPeer }) => {
`${scheme}://${hostname}:${port}/raw/rad:z2J7s48EbCBckcEmj2dm5eaFVoBsy/blobs/bae036309c2182c7304c97956969369823b5c6ad?mime=image/png`,
);
await expect(
page.locator(".badge").filter({ hasText: "radicle-228x228.png" }),
page.getByRole("button", { name: "radicle-228x228.png" }),
).toBeVisible();
await page.getByRole("button", { name: "edit comment" }).click();
@@ -155,9 +155,9 @@ test("handling embeds", async ({ page, authenticatedPeer }) => {
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("button", { name: "Save" })).toBeHidden();
await expect(
page.locator(".badge").filter({ hasText: "apple-touch-icon.png" }),
page.getByRole("button", { name: "apple-touch-icon.png" }),
).toBeVisible();
await expect(
page.locator(".badge").filter({ hasText: "radicle-228x228.png" }),
page.getByRole("button", { name: "radicle-228x228.png" }),
).toBeVisible();
});
+3 -3
View File
@@ -19,7 +19,7 @@ test("theme persistance", async ({ page }) => {
await expect(
page.getByRole("banner").getByRole("link", { name: "source-browsing" }),
).toBeVisible();
await page.getByText("Settings").click();
await page.getByRole("button", { name: "Settings" }).click();
await page.getByText("System").click();
await page.getByRole("button", { name: "Light Mode" }).click();
@@ -38,7 +38,7 @@ test("change theme", async ({ page }) => {
await expect(
page.getByRole("banner").getByRole("link", { name: "source-browsing" }),
).toBeVisible();
await page.getByText("Settings").click();
await page.getByRole("button", { name: "Settings" }).click();
await page.getByRole("button", { name: "Light Mode" }).click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
@@ -65,7 +65,7 @@ test("change code font", async ({ page }) => {
page.getByRole("banner").getByRole("link", { name: "source-browsing" }),
).toBeVisible();
await page.getByText("Settings").click();
await page.getByRole("button", { name: "Settings" }).click();
await page.getByText("System").click();
await expect(page.getByText("System")).toHaveClass(/selected/);