Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Extract top-level menu into a layout
Rūdolfs Ošiņš committed 2 years ago
commit 723509a2671b444b23250142e0db551954b92cba
parent 0146ca00b5cdd38eafd846e0b500a37c63df66e1
11 files changed +684 -649
modified src/views/projects/Browser.svelte
@@ -7,6 +7,7 @@
  import { HttpdClient } from "@httpd-client";

  import Button from "@app/components/Button.svelte";
+
  import Layout from "./Layout.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
  import SourceBrowsingHeader from "./SourceBrowsingHeader.svelte";

@@ -14,6 +15,7 @@
  import TreeComponent from "./Tree.svelte";

  export let baseUrl: BaseUrl;
+
  export let blobResult: BlobResult;
  export let branches: string[];
  export let path: string;
  export let peer: string | undefined;
@@ -21,7 +23,6 @@
  export let project: Project;
  export let revision: string | undefined;
  export let tree: Tree;
-
  export let blobResult: BlobResult;

  // Whether the mobile file tree is visible.
  let mobileFileTree = false;
@@ -145,81 +146,85 @@
  }
</style>

-
<SourceBrowsingHeader
-
  node={baseUrl}
-
  {project}
-
  peers={peersWithRoute}
-
  branches={branchesWithRoute}
-
  {revision}
-
  {tree}
-
  historyLinkActive={false} />
-

-
<main>
-
  <!-- Mobile navigation -->
-
  {#if tree.entries.length > 0}
-
    <nav class="layout-mobile">
-
      <Button
-
        style="width: 100%;"
-
        variant="secondary"
-
        on:click={() => {
-
          mobileFileTree = !mobileFileTree;
-
        }}>
-
        Browse
-
      </Button>
-
    </nav>
-
  {/if}
-

-
  <div class="container center-content">
+
<Layout {baseUrl} {project} {peer} activeTab="source">
+
  <SourceBrowsingHeader
+
    node={baseUrl}
+
    {project}
+
    peers={peersWithRoute}
+
    branches={branchesWithRoute}
+
    {revision}
+
    {tree}
+
    historyLinkActive={false} />
+

+
  <main>
+
    <!-- Mobile navigation -->
    {#if tree.entries.length > 0}
-
      <div class="column-left" class:column-left-visible={mobileFileTree}>
-
        <div class="source-tree sticky">
-
          <TreeComponent
-
            projectId={project.id}
-
            {revision}
-
            {baseUrl}
-
            {fetchTree}
-
            {path}
-
            {peer}
-
            {tree}
-
            on:select={() => {
-
              // Close mobile tree if user navigates to other file.
-
              mobileFileTree = false;
-
            }} />
+
      <nav class="layout-mobile">
+
        <Button
+
          style="width: 100%;"
+
          variant="secondary"
+
          on:click={() => {
+
            mobileFileTree = !mobileFileTree;
+
          }}>
+
          Browse
+
        </Button>
+
      </nav>
+
    {/if}
+

+
    <div class="container center-content">
+
      {#if tree.entries.length > 0}
+
        <div class="column-left" class:column-left-visible={mobileFileTree}>
+
          <div class="source-tree sticky">
+
            <TreeComponent
+
              projectId={project.id}
+
              {revision}
+
              {baseUrl}
+
              {fetchTree}
+
              {path}
+
              {peer}
+
              {tree}
+
              on:select={() => {
+
                // Close mobile tree if user navigates to other file.
+
                mobileFileTree = false;
+
              }} />
+
          </div>
        </div>
-
      </div>
-
      <div class="column-right">
-
        {#if blobResult.ok}
-
          <BlobComponent
-
            {baseUrl}
-
            projectId={project.id}
-
            {peer}
-
            {revision}
-
            {path}
-
            blob={blobResult.blob}
-
            highlighted={blobResult.highlighted}
-
            rawPath={utils.getRawBasePath(
-
              project.id,
-
              baseUrl,
-
              tree.lastCommit.id,
-
            )} />
-
        {:else}
-
          <Placeholder emoji="🍂">
-
            <span slot="title">
-
              <div class="txt-monospace">{blobResult.error.path}</div>
-
            </span>
+
        <div class="column-right">
+
          {#if blobResult.ok}
+
            <BlobComponent
+
              {baseUrl}
+
              projectId={project.id}
+
              {peer}
+
              {revision}
+
              {path}
+
              blob={blobResult.blob}
+
              highlighted={blobResult.highlighted}
+
              rawPath={utils.getRawBasePath(
+
                project.id,
+
                baseUrl,
+
                tree.lastCommit.id,
+
              )} />
+
          {:else}
+
            <Placeholder emoji="🍂">
+
              <span slot="title">
+
                <div class="txt-monospace">{blobResult.error.path}</div>
+
              </span>
+
              <span slot="body">
+
                {blobResult.error.message}
+
              </span>
+
            </Placeholder>
+
          {/if}
+
        </div>
+
      {:else}
+
        <div class="placeholder">
+
          <Placeholder emoji="👀">
+
            <span slot="title">Nothing to show</span>
            <span slot="body">
-
              {blobResult.error.message}
+
              We couldn't find any files at this revision.
            </span>
          </Placeholder>
-
        {/if}
-
      </div>
-
    {:else}
-
      <div class="placeholder">
-
        <Placeholder emoji="👀">
-
          <span slot="title">Nothing to show</span>
-
          <span slot="body">We couldn't find any files at this revision.</span>
-
        </Placeholder>
-
      </div>
-
    {/if}
-
  </div>
-
</main>
+
        </div>
+
      {/if}
+
    </div>
+
  </main>
+
</Layout>
modified src/views/projects/Commit.svelte
@@ -7,6 +7,7 @@
  import Clipboard from "@app/components/Clipboard.svelte";
  import CommitAuthorship from "@app/views/projects/Commit/CommitAuthorship.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
+
  import Layout from "./Layout.svelte";

  export let baseUrl: BaseUrl;
  export let commit: Commit;
@@ -49,27 +50,29 @@
  }
</style>

-
<div class="commit">
-
  <div class="header">
-
    <div class="summary">
-
      <div class="txt-medium txt-bold">
-
        <InlineMarkdown fontSize="medium" content={header.summary} />
-
      </div>
-
      <div class="layout-desktop-flex txt-monospace sha1">
-
        <span>{header.id}</span>
-
        <Clipboard small text={header.id} />
-
      </div>
-
      <div class="layout-mobile-flex txt-monospace sha1 txt-small">
-
        {formatCommit(header.id)}
-
        <Clipboard small text={header.id} />
+
<Layout {baseUrl} {project}>
+
  <div class="commit">
+
    <div class="header">
+
      <div class="summary">
+
        <div class="txt-medium txt-bold">
+
          <InlineMarkdown fontSize="medium" content={header.summary} />
+
        </div>
+
        <div class="layout-desktop-flex txt-monospace sha1">
+
          <span>{header.id}</span>
+
          <Clipboard small text={header.id} />
+
        </div>
+
        <div class="layout-mobile-flex txt-monospace sha1 txt-small">
+
          {formatCommit(header.id)}
+
          <Clipboard small text={header.id} />
+
        </div>
      </div>
+
      <pre class="description txt-small">{header.description}</pre>
+
      <CommitAuthorship {header} />
    </div>
-
    <pre class="description txt-small">{header.description}</pre>
-
    <CommitAuthorship {header} />
+
    <Changeset
+
      projectId={project.id}
+
      {baseUrl}
+
      diff={commit.diff}
+
      revision={commit.commit.id} />
  </div>
-
  <Changeset
-
    projectId={project.id}
-
    {baseUrl}
-
    diff={commit.diff}
-
    revision={commit.commit.id} />
-
</div>
+
</Layout>
modified src/views/projects/Header.svelte
@@ -1,6 +1,9 @@
+
<script lang="ts" context="module">
+
  export type ActiveTab = "source" | "issues" | "patches" | undefined;
+
</script>
+

<script lang="ts">
  import type { BaseUrl } from "@httpd-client";
-
  import type { ProjectLoadedView } from "@app/views/projects/router";

  import { isLocal } from "@app/lib/utils";
  import { pluralize } from "@app/lib/pluralize";
@@ -10,8 +13,8 @@
  import Link from "@app/components/Link.svelte";
  import SquareButton from "@app/components/SquareButton.svelte";

-
  export let resource: ProjectLoadedView["resource"];
  export let baseUrl: BaseUrl;
+
  export let activeTab: ActiveTab = undefined;

  export let projectId: string;
  export let projectName: string;
@@ -48,7 +51,7 @@
      node: baseUrl,
      path: "/",
    }}>
-
    <SquareButton active={resource === "tree" || resource === "history"}>
+
    <SquareButton active={activeTab === "source"}>
      <svelte:fragment slot="icon">
        <Icon size="small" name="chevron-left-right" />
      </svelte:fragment>
@@ -61,7 +64,7 @@
      project: projectId,
      node: baseUrl,
    }}>
-
    <SquareButton active={resource === "issues" || resource === "issue"}>
+
    <SquareButton active={activeTab === "issues"}>
      <svelte:fragment slot="icon">
        <Icon size="small" name="exclamation-circle" />
      </svelte:fragment>
@@ -76,7 +79,7 @@
      project: projectId,
      node: baseUrl,
    }}>
-
    <SquareButton active={resource === "patches" || resource === "patch"}>
+
    <SquareButton active={activeTab === "patches"}>
      <svelte:fragment slot="icon">
        <Icon size="small" name="patch" />
      </svelte:fragment>
modified src/views/projects/History.svelte
@@ -14,6 +14,7 @@
  import Button from "@app/components/Button.svelte";
  import CommitTeaser from "./Commit/CommitTeaser.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
+
  import Layout from "./Layout.svelte";
  import Loading from "@app/components/Loading.svelte";
  import SourceBrowsingHeader from "./SourceBrowsingHeader.svelte";
  import { COMMITS_PER_PAGE } from "./router";
@@ -108,37 +109,39 @@
  }
</style>

-
<SourceBrowsingHeader
-
  node={baseUrl}
-
  {project}
-
  peers={peersWithRoute}
-
  branches={branchesWithRoute}
-
  {revision}
-
  {tree}
-
  historyLinkActive={true} />
+
<Layout {baseUrl} {project} {peer} activeTab="source">
+
  <SourceBrowsingHeader
+
    node={baseUrl}
+
    {project}
+
    peers={peersWithRoute}
+
    branches={branchesWithRoute}
+
    {revision}
+
    {tree}
+
    historyLinkActive={true} />

-
<div class="history">
-
  {#each groupCommits(allCommitHeaders) as group (group.time)}
-
    <p style:color="var(--color-foreground-6)">{group.date}</p>
-
    <div class="group">
-
      {#each group.commits as commit (commit.id)}
-
        <div class="teaser-wrapper">
-
          <CommitTeaser projectId={project.id} {baseUrl} {commit} />
-
        </div>
-
      {/each}
+
  <div class="history">
+
    {#each groupCommits(allCommitHeaders) as group (group.time)}
+
      <p style:color="var(--color-foreground-6)">{group.date}</p>
+
      <div class="group">
+
        {#each group.commits as commit (commit.id)}
+
          <div class="teaser-wrapper">
+
            <CommitTeaser projectId={project.id} {baseUrl} {commit} />
+
          </div>
+
        {/each}
+
      </div>
+
    {/each}
+
    <div class="more">
+
      {#if loading}
+
        <Loading small={page !== 0} center />
+
      {:else if allCommitHeaders.length < totalCommitCount}
+
        <Button variant="foreground" on:click={loadMore}>More</Button>
+
      {/if}
    </div>
-
  {/each}
-
  <div class="more">
-
    {#if loading}
-
      <Loading small={page !== 0} center />
-
    {:else if allCommitHeaders.length < totalCommitCount}
-
      <Button variant="foreground" on:click={loadMore}>More</Button>
-
    {/if}
  </div>
-
</div>

-
{#if error}
-
  <div class="message">
-
    <ErrorMessage message="Couldn't load commits." stackTrace={error} />
-
  </div>
-
{/if}
+
  {#if error}
+
    <div class="message">
+
      <ErrorMessage message="Couldn't load commits." stackTrace={error} />
+
    </div>
+
  {/if}
+
</Layout>
modified src/views/projects/Issue.svelte
@@ -20,13 +20,14 @@
  import CobStateButton from "@app/views/projects/Cob/CobStateButton.svelte";
  import ErrorModal from "@app/views/projects/Cob/ErrorModal.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import Markdown from "@app/components/Markdown.svelte";
  import LabelInput from "./Cob/LabelInput.svelte";
+
  import Layout from "./Layout.svelte";
+
  import Markdown from "@app/components/Markdown.svelte";
  import Textarea from "@app/components/Textarea.svelte";
  import ThreadComponent from "@app/components/Thread.svelte";

-
  export let issue: Issue;
  export let baseUrl: BaseUrl;
+
  export let issue: Issue;
  export let project: Project;

  const rawPath = utils.getRawBasePath(project.id, baseUrl, project.head);
@@ -308,97 +309,99 @@
  }
</style>

-
<div class="issue">
-
  <div>
-
    <CobHeader
-
      {action}
-
      id={issue.id}
-
      title={issue.title}
-
      on:editTitle={editTitle}>
-
      <svelte:fragment slot="icon">
-
        <div
-
          class="state"
-
          class:closed={issue.state.status === "closed"}
-
          class:open={issue.state.status === "open"}>
-
          <Icon name="exclamation-circle" />
-
        </div>
-
      </svelte:fragment>
-
      <svelte:fragment slot="state">
-
        {#if issue.state.status === "open"}
-
          <Badge variant="positive">
-
            {issue.state.status}
-
          </Badge>
-
        {:else}
-
          <Badge variant="negative">
-
            {issue.state.status} as
-
            {issue.state.reason}
-
          </Badge>
-
        {/if}
-
      </svelte:fragment>
-
      <div slot="description">
-
        <Markdown
-
          content={issue.discussion[0].body}
-
          rawPath={utils.getRawBasePath(project.id, baseUrl, project.head)} />
-
        {#if issue.discussion[0].reactions}
-
          <div class="reactions txt-tiny">
-
            {#each groupedReactions as [reaction, nids], key}
-
              <Chip {key}>
-
                <div class="reaction">
-
                  <span>{reaction}</span>
-
                  <span title={nids.join("\n")}>{nids.length}</span>
-
                </div>
-
              </Chip>
-
            {/each}
+
<Layout {baseUrl} {project} activeTab="issues">
+
  <div class="issue">
+
    <div>
+
      <CobHeader
+
        {action}
+
        id={issue.id}
+
        title={issue.title}
+
        on:editTitle={editTitle}>
+
        <svelte:fragment slot="icon">
+
          <div
+
            class="state"
+
            class:closed={issue.state.status === "closed"}
+
            class:open={issue.state.status === "open"}>
+
            <Icon name="exclamation-circle" />
          </div>
-
        {/if}
-
      </div>
-
      <div class="author" slot="author">
-
        opened by <Authorship
-
          authorId={issue.author.id}
-
          authorAlias={issue.author.alias} />
-
        {utils.formatTimestamp(issue.discussion[0].timestamp)}
-
      </div>
-
    </CobHeader>
-
    {#each threads as thread (thread.root.id)}
-
      <div class="thread">
-
        <ThreadComponent {thread} {rawPath} on:reply={createReply} />
-
      </div>
-
    {/each}
-
    {#if $httpdStore.state === "authenticated"}
-
      <div style:margin-top="1rem">
-
        <Textarea
-
          resizable
-
          on:submit={async () => {
-
            await createComment(commentBody);
-
            commentBody = "";
-
          }}
-
          bind:value={commentBody}
-
          placeholder="Leave your comment" />
-
        <div class="actions txt-small">
-
          <CobStateButton
-
            items={items.filter(([, state]) => !isEqual(state, issue.state))}
-
            {selectedItem}
-
            state={issue.state}
-
            on:saveStatus={saveStatus} />
-
          <Button
-
            variant="secondary"
-
            size="small"
-
            disabled={!commentBody}
-
            on:click={async () => {
+
        </svelte:fragment>
+
        <svelte:fragment slot="state">
+
          {#if issue.state.status === "open"}
+
            <Badge variant="positive">
+
              {issue.state.status}
+
            </Badge>
+
          {:else}
+
            <Badge variant="negative">
+
              {issue.state.status} as
+
              {issue.state.reason}
+
            </Badge>
+
          {/if}
+
        </svelte:fragment>
+
        <div slot="description">
+
          <Markdown
+
            content={issue.discussion[0].body}
+
            rawPath={utils.getRawBasePath(project.id, baseUrl, project.head)} />
+
          {#if issue.discussion[0].reactions}
+
            <div class="reactions txt-tiny">
+
              {#each groupedReactions as [reaction, nids], key}
+
                <Chip {key}>
+
                  <div class="reaction">
+
                    <span>{reaction}</span>
+
                    <span title={nids.join("\n")}>{nids.length}</span>
+
                  </div>
+
                </Chip>
+
              {/each}
+
            </div>
+
          {/if}
+
        </div>
+
        <div class="author" slot="author">
+
          opened by <Authorship
+
            authorId={issue.author.id}
+
            authorAlias={issue.author.alias} />
+
          {utils.formatTimestamp(issue.discussion[0].timestamp)}
+
        </div>
+
      </CobHeader>
+
      {#each threads as thread (thread.root.id)}
+
        <div class="thread">
+
          <ThreadComponent {thread} {rawPath} on:reply={createReply} />
+
        </div>
+
      {/each}
+
      {#if $httpdStore.state === "authenticated"}
+
        <div style:margin-top="1rem">
+
          <Textarea
+
            resizable
+
            on:submit={async () => {
              await createComment(commentBody);
              commentBody = "";
-
            }}>
-
            Comment
-
          </Button>
+
            }}
+
            bind:value={commentBody}
+
            placeholder="Leave your comment" />
+
          <div class="actions txt-small">
+
            <CobStateButton
+
              items={items.filter(([, state]) => !isEqual(state, issue.state))}
+
              {selectedItem}
+
              state={issue.state}
+
              on:saveStatus={saveStatus} />
+
            <Button
+
              variant="secondary"
+
              size="small"
+
              disabled={!commentBody}
+
              on:click={async () => {
+
                await createComment(commentBody);
+
                commentBody = "";
+
              }}>
+
              Comment
+
            </Button>
+
          </div>
        </div>
-
      </div>
-
    {/if}
-
  </div>
-
  <div class="metadata">
-
    <AssigneeInput
-
      {action}
-
      assignees={issue.assignees}
-
      on:save={saveAssignees} />
-
    <LabelInput {action} labels={issue.labels} on:save={saveLabels} />
+
      {/if}
+
    </div>
+
    <div class="metadata">
+
      <AssigneeInput
+
        {action}
+
        assignees={issue.assignees}
+
        on:save={saveAssignees} />
+
      <LabelInput {action} labels={issue.labels} on:save={saveLabels} />
+
    </div>
  </div>
-
</div>
+
</Layout>
modified src/views/projects/Issue/New.svelte
@@ -13,13 +13,14 @@
  import Badge from "@app/components/Badge.svelte";
  import Button from "@app/components/Button.svelte";
  import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
-
  import Markdown from "@app/components/Markdown.svelte";
+
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";
+
  import Layout from "@app/views/projects/Layout.svelte";
+
  import Markdown from "@app/components/Markdown.svelte";
  import Textarea from "@app/components/Textarea.svelte";
-
  import ErrorMessage from "@app/components/ErrorMessage.svelte";

-
  export let project: Project;
  export let baseUrl: BaseUrl;
+
  export let project: Project;

  let preview: boolean = false;
  let action: "create" | "view";
@@ -124,76 +125,79 @@
  }
</style>

-
<main>
-
  {#if $httpdStore.state === "authenticated"}
-
    {@const session = $httpdStore.session}
-
    <div class="form">
-
      <div class="editor">
-
        <CobHeader {action} bind:title={issueTitle}>
-
          <svelte:fragment slot="state">
-
            {#if action === "view"}
-
              <Badge variant="positive">open</Badge>
-
            {/if}
-
          </svelte:fragment>
-
          <svelte:fragment slot="description">
-
            {#if action === "create"}
-
              <Textarea
-
                resizable
-
                bind:value={issueText}
-
                on:submit={() => {
-
                  void createIssue(session.id);
-
                }}
-
                placeholder="Write a description" />
-
            {:else if !issueText}
-
              <p class="txt-missing">No description</p>
-
            {:else}
-
              <Markdown
-
                content={issueText}
-
                rawPath={utils.getRawBasePath(
-
                  project.id,
-
                  baseUrl,
-
                  project.head,
-
                )} />
-
            {/if}
-
          </svelte:fragment>
-
          <div class="author" slot="author">
-
            {#if action === "view"}
-
              opened by <Authorship authorId={$httpdStore.session.publicKey} /> now
-
            {/if}
+
<Layout {baseUrl} {project} activeTab="issues">
+
  <main>
+
    {#if $httpdStore.state === "authenticated"}
+
      {@const session = $httpdStore.session}
+
      <div class="form">
+
        <div class="editor">
+
          <CobHeader {action} bind:title={issueTitle}>
+
            <svelte:fragment slot="state">
+
              {#if action === "view"}
+
                <Badge variant="positive">open</Badge>
+
              {/if}
+
            </svelte:fragment>
+
            <svelte:fragment slot="description">
+
              {#if action === "create"}
+
                <Textarea
+
                  resizable
+
                  bind:value={issueText}
+
                  on:submit={() => {
+
                    void createIssue(session.id);
+
                  }}
+
                  placeholder="Write a description" />
+
              {:else if !issueText}
+
                <p class="txt-missing">No description</p>
+
              {:else}
+
                <Markdown
+
                  content={issueText}
+
                  rawPath={utils.getRawBasePath(
+
                    project.id,
+
                    baseUrl,
+
                    project.head,
+
                  )} />
+
              {/if}
+
            </svelte:fragment>
+
            <div class="author" slot="author">
+
              {#if action === "view"}
+
                opened by <Authorship
+
                  authorId={$httpdStore.session.publicKey} /> now
+
              {/if}
+
            </div>
+
          </CobHeader>
+
          <div class="actions">
+
            <Button
+
              size="small"
+
              variant="text"
+
              on:click={() => (preview = !preview)}>
+
              {#if preview}
+
                Resume editing
+
              {:else}
+
                Preview
+
              {/if}
+
            </Button>
+
            <Button
+
              disabled={!issueTitle || !issueText}
+
              size="small"
+
              variant="secondary"
+
              on:click={() => void createIssue(session.id)}>
+
              Submit
+
            </Button>
          </div>
-
        </CobHeader>
-
        <div class="actions">
-
          <Button
-
            size="small"
-
            variant="text"
-
            on:click={() => (preview = !preview)}>
-
            {#if preview}
-
              Resume editing
-
            {:else}
-
              Preview
-
            {/if}
-
          </Button>
-
          <Button
-
            disabled={!issueTitle || !issueText}
-
            size="small"
-
            variant="secondary"
-
            on:click={() => void createIssue(session.id)}>
-
            Submit
-
          </Button>
+
        </div>
+
        <div class="metadata">
+
          <AssigneeInput
+
            {action}
+
            on:save={({ detail: updatedAssignees }) =>
+
              (assignees = updatedAssignees)} />
+
          <LabelInput
+
            {action}
+
            on:save={({ detail: updatedLabels }) => (labels = updatedLabels)} />
        </div>
      </div>
-
      <div class="metadata">
-
        <AssigneeInput
-
          {action}
-
          on:save={({ detail: updatedAssignees }) =>
-
            (assignees = updatedAssignees)} />
-
        <LabelInput
-
          {action}
-
          on:save={({ detail: updatedLabels }) => (labels = updatedLabels)} />
-
      </div>
-
    </div>
-
  {:else}
-
    <ErrorMessage
-
      message="Couldn't access issue creation. Make sure you're still logged in." />
-
  {/if}
-
</main>
+
    {:else}
+
      <ErrorMessage
+
        message="Couldn't access issue creation. Make sure you're still logged in." />
+
    {/if}
+
  </main>
+
</Layout>
modified src/views/projects/Issues.svelte
@@ -10,14 +10,15 @@
  import Button from "@app/components/Button.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import IssueTeaser from "@app/views/projects/Issue/IssueTeaser.svelte";
+
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import Loading from "@app/components/Loading.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
  import SquareButton from "@app/components/SquareButton.svelte";

-
  export let project: Project;
  export let baseUrl: BaseUrl;
  export let issues: Issue[];
+
  export let project: Project;
  export let state: IssueState["status"];

  let loading = false;
@@ -97,75 +98,77 @@
  }
</style>

-
<div class="issues">
-
  <div class="section-header">
-
    <div style="margin-bottom: 2rem;">
-
      <div style="display: flex; gap: 0.5rem;">
-
        {#each options as option}
-
          {#if !option.disabled}
-
            <Link
-
              route={{
-
                resource: "project.issues",
-
                project: project.id,
-
                node: baseUrl,
-
                state: option.value,
-
              }}>
+
<Layout {baseUrl} {project} activeTab="issues">
+
  <div class="issues">
+
    <div class="section-header">
+
      <div style="margin-bottom: 2rem;">
+
        <div style="display: flex; gap: 0.5rem;">
+
          {#each options as option}
+
            {#if !option.disabled}
+
              <Link
+
                route={{
+
                  resource: "project.issues",
+
                  project: project.id,
+
                  node: baseUrl,
+
                  state: option.value,
+
                }}>
+
                <SquareButton
+
                  clickable={option.disabled}
+
                  active={option.value === state}
+
                  disabled={option.disabled}>
+
                  {option.title}
+
                </SquareButton>
+
              </Link>
+
            {:else}
              <SquareButton
                clickable={option.disabled}
                active={option.value === state}
                disabled={option.disabled}>
                {option.title}
              </SquareButton>
-
            </Link>
-
          {:else}
-
            <SquareButton
-
              clickable={option.disabled}
-
              active={option.value === state}
-
              disabled={option.disabled}>
-
              {option.title}
-
            </SquareButton>
-
          {/if}
-
        {/each}
+
            {/if}
+
          {/each}
+
        </div>
      </div>
+
      {#if $httpdStore.state === "authenticated" && utils.isLocal(baseUrl.hostname)}
+
        <Link
+
          route={{
+
            resource: "project.newIssue",
+
            project: project.id,
+
            node: baseUrl,
+
          }}>
+
          <SquareButton>New issue</SquareButton>
+
        </Link>
+
      {/if}
    </div>
-
    {#if $httpdStore.state === "authenticated" && utils.isLocal(baseUrl.hostname)}
-
      <Link
-
        route={{
-
          resource: "project.newIssue",
-
          project: project.id,
-
          node: baseUrl,
-
        }}>
-
        <SquareButton>New issue</SquareButton>
-
      </Link>
-
    {/if}
-
  </div>
-
  <div class="issues-list">
-
    {#each allIssues as issue (issue.id)}
-
      <div class="teaser">
-
        <IssueTeaser projectId={project.id} {baseUrl} {issue} />
-
      </div>
-
    {:else}
-
      {#if error}
-
        <ErrorMessage message="Couldn't load issues." stackTrace={error} />
-
      {:else if loading}
-
        <!-- We already show a loader below. -->
+
    <div class="issues-list">
+
      {#each allIssues as issue (issue.id)}
+
        <div class="teaser">
+
          <IssueTeaser projectId={project.id} {baseUrl} {issue} />
+
        </div>
      {:else}
-
        <Placeholder emoji="🍂">
-
          <div slot="title">{capitalize(state)} issues</div>
-
          <div slot="body">No issues matched the current filter</div>
-
        </Placeholder>
+
        {#if error}
+
          <ErrorMessage message="Couldn't load issues." stackTrace={error} />
+
        {:else if loading}
+
          <!-- We already show a loader below. -->
+
        {:else}
+
          <Placeholder emoji="🍂">
+
            <div slot="title">{capitalize(state)} issues</div>
+
            <div slot="body">No issues matched the current filter</div>
+
          </Placeholder>
+
        {/if}
+
      {/each}
+
    </div>
+
    <div class="more">
+
      {#if loading}
+
        <Loading small={page !== 0} center />
      {/if}
-
    {/each}
-
  </div>
-
  <div class="more">
-
    {#if loading}
-
      <Loading small={page !== 0} center />
-
    {/if}

-
    {#if showMoreButton}
-
      <Button variant="foreground" on:click={() => loadIssues(state)}>
-
        More
-
      </Button>
-
    {/if}
+
      {#if showMoreButton}
+
        <Button variant="foreground" on:click={() => loadIssues(state)}>
+
          More
+
        </Button>
+
      {/if}
+
    </div>
  </div>
-
</div>
+
</Layout>
added src/views/projects/Layout.svelte
@@ -0,0 +1,48 @@
+
<script lang="ts">
+
  import type { ActiveTab } from "./Header.svelte";
+
  import type { BaseUrl, Project } from "@httpd-client";
+

+
  import Header from "./Header.svelte";
+
  import ProjectMeta from "./ProjectMeta.svelte";
+

+
  export let baseUrl: BaseUrl;
+
  export let project: Project;
+
  export let peer: string | undefined = undefined;
+
  export let activeTab: ActiveTab = undefined;
+
</script>
+

+
<style>
+
  .header {
+
    width: 100%;
+
    max-width: var(--content-max-width);
+
    min-width: var(--content-min-width);
+
    padding-top: 4rem;
+
  }
+
  main {
+
    width: 100%;
+
    max-width: var(--content-max-width);
+
    min-width: var(--content-min-width);
+
    padding-bottom: 4rem;
+
  }
+
</style>
+

+
<div class="header">
+
  <ProjectMeta
+
    nodeId={peer}
+
    projectDescription={project.description}
+
    projectId={project.id}
+
    projectName={project.name}
+
    {baseUrl} />
+
  <Header
+
    openIssueCount={project.issues.open}
+
    openPatchCount={project.patches.open}
+
    projectId={project.id}
+
    projectName={project.name}
+
    trackings={project.trackings}
+
    {activeTab}
+
    {baseUrl} />
+
</div>
+

+
<main>
+
  <slot />
+
</main>
modified src/views/projects/Patch.svelte
@@ -29,9 +29,9 @@

<script lang="ts">
  import type { BaseUrl, Patch } from "@httpd-client";
-
  import type { Variant } from "@app/components/Badge.svelte";
-
  import { type Route } from "@app/lib/router";
  import type { PatchView } from "./router";
+
  import type { Route } from "@app/lib/router";
+
  import type { Variant } from "@app/components/Badge.svelte";

  import * as utils from "@app/lib/utils";
  import { capitalize, isEqual } from "lodash";
@@ -47,16 +47,17 @@
  import DropdownItem from "@app/components/Dropdown/DropdownItem.svelte";
  import Floating, { closeFocused } from "@app/components/Floating.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";
+
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import Markdown from "@app/components/Markdown.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
  import RevisionComponent from "@app/views/projects/Cob/Revision.svelte";
  import SquareButton from "@app/components/SquareButton.svelte";
-
  import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";

  export let baseUrl: BaseUrl;
-
  export let project: Project;
  export let patch: Patch;
+
  export let project: Project;
  export let view: PatchView["view"];

  $: api = new HttpdClient(baseUrl);
@@ -305,190 +306,192 @@
  }
</style>

-
<div class="patch">
-
  <div>
-
    <CobHeader id={patch.id} title={patch.title}>
-
      <svelte:fragment slot="icon">
-
        <div
-
          class="state"
-
          class:draft={patch.state.status === "draft"}
-
          class:open={patch.state.status === "open"}
-
          class:merged={patch.state.status === "merged"}
-
          class:archived={patch.state.status === "archived"}>
-
          <Icon name="patch" />
+
<Layout {baseUrl} {project} activeTab="patches">
+
  <div class="patch">
+
    <div>
+
      <CobHeader id={patch.id} title={patch.title}>
+
        <svelte:fragment slot="icon">
+
          <div
+
            class="state"
+
            class:draft={patch.state.status === "draft"}
+
            class:open={patch.state.status === "open"}
+
            class:merged={patch.state.status === "merged"}
+
            class:archived={patch.state.status === "archived"}>
+
            <Icon name="patch" />
+
          </div>
+
        </svelte:fragment>
+
        <svelte:fragment slot="state">
+
          <Badge variant={badgeColor(patch.state.status)}>
+
            {patch.state.status}
+
          </Badge>
+
        </svelte:fragment>
+
        <svelte:fragment slot="description">
+
          {#if patch.revisions[0].description}
+
            <Markdown
+
              content={patch.revisions[0].description}
+
              rawPath={utils.getRawBasePath(
+
                project.id,
+
                baseUrl,
+
                patch.revisions[0].id,
+
              )} />
+
          {:else}
+
            <span class="txt-missing">No description available</span>
+
          {/if}
+
        </svelte:fragment>
+
        <div class="author" slot="author">
+
          opened by <Authorship
+
            authorId={patch.author.id}
+
            authorAlias={patch.author.alias} />
+
          {utils.formatTimestamp(patch.revisions[0].timestamp)}
+
        </div>
+
      </CobHeader>
+

+
      <div class="tab-line">
+
        <div style="display: flex; gap: 0.5rem;">
+
          {#each Object.entries(tabs) as [name, route]}
+
            <Link {route}>
+
              <SquareButton size="small" active={name === view.name}>
+
                {capitalize(name)}
+
              </SquareButton>
+
            </Link>
+
          {/each}
+
          {#if view.name === "diff"}
+
            <Link
+
              route={{
+
                resource: "project.patch",
+
                project: project.id,
+
                node: baseUrl,
+
                patch: patch.id,
+
                view: {
+
                  name: "diff",
+
                  fromCommit: view.fromCommit,
+
                  toCommit: view.toCommit,
+
                },
+
              }}>
+
              <SquareButton size="small" active={true}>
+
                Diff {view.fromCommit.substring(
+
                  0,
+
                  6,
+
                )}..{view.toCommit.substring(0, 6)}
+
              </SquareButton>
+
            </Link>
+
          {/if}
        </div>
-
      </svelte:fragment>
-
      <svelte:fragment slot="state">
-
        <Badge variant={badgeColor(patch.state.status)}>
-
          {patch.state.status}
-
        </Badge>
-
      </svelte:fragment>
-
      <svelte:fragment slot="description">
-
        {#if patch.revisions[0].description}
-
          <Markdown
-
            content={patch.revisions[0].description}
-
            rawPath={utils.getRawBasePath(
-
              project.id,
-
              baseUrl,
-
              patch.revisions[0].id,
-
            )} />
-
        {:else}
-
          <span class="txt-missing">No description available</span>
-
        {/if}
-
      </svelte:fragment>
-
      <div class="author" slot="author">
-
        opened by <Authorship
-
          authorId={patch.author.id}
-
          authorAlias={patch.author.alias} />
-
        {utils.formatTimestamp(patch.revisions[0].timestamp)}
-
      </div>
-
    </CobHeader>

-
    <div class="tab-line">
-
      <div style="display: flex; gap: 0.5rem;">
-
        {#each Object.entries(tabs) as [name, route]}
-
          <Link {route}>
-
            <SquareButton size="small" active={name === view.name}>
-
              {capitalize(name)}
-
            </SquareButton>
-
          </Link>
-
        {/each}
-
        {#if view.name === "diff"}
-
          <Link
-
            route={{
-
              resource: "project.patch",
-
              project: project.id,
-
              node: baseUrl,
-
              patch: patch.id,
-
              view: {
-
                name: "diff",
-
                fromCommit: view.fromCommit,
-
                toCommit: view.toCommit,
-
              },
-
            }}>
-
            <SquareButton size="small" active={true}>
-
              Diff {view.fromCommit.substring(0, 6)}..{view.toCommit.substring(
-
                0,
-
                6,
-
              )}
-
            </SquareButton>
-
          </Link>
+
        {#if view.name === "commits" || view.name === "files"}
+
          <Floating disabled={patch.revisions.length === 1}>
+
            <svelte:fragment slot="toggle">
+
              <SquareButton
+
                size="small"
+
                clickable={patch.revisions.length > 1}
+
                disabled={patch.revisions.length === 1}>
+
                Revision {utils.formatObjectId(view.revision)}
+
              </SquareButton>
+
            </svelte:fragment>
+
            <svelte:fragment slot="modal">
+
              <Dropdown 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,
+
                      },
+
                    }}>
+
                    <DropdownItem
+
                      selected={item.id === view.revision}
+
                      size="tiny">
+
                      Revision {utils.formatObjectId(item.id)}
+
                    </DropdownItem>
+
                  </Link>
+
                </svelte:fragment>
+
              </Dropdown>
+
            </svelte:fragment>
+
          </Floating>
        {/if}
      </div>
-

-
      {#if view.name === "commits" || view.name === "files"}
-
        <Floating disabled={patch.revisions.length === 1}>
-
          <svelte:fragment slot="toggle">
-
            <SquareButton
-
              size="small"
-
              clickable={patch.revisions.length > 1}
-
              disabled={patch.revisions.length === 1}>
-
              Revision {utils.formatObjectId(view.revision)}
-
            </SquareButton>
-
          </svelte:fragment>
-
          <svelte:fragment slot="modal">
-
            <Dropdown 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,
-
                    },
-
                  }}>
-
                  <DropdownItem
-
                    selected={item.id === view.revision}
-
                    size="tiny">
-
                    Revision {utils.formatObjectId(item.id)}
-
                  </DropdownItem>
-
                </Link>
-
              </svelte:fragment>
-
            </Dropdown>
-
          </svelte:fragment>
-
        </Floating>
+
      {#if view.name === "diff"}
+
        <div style:margin-top="1rem">
+
          <Changeset
+
            projectId={project.id}
+
            {baseUrl}
+
            revision={view.toCommit}
+
            diff={view.diff} />
+
        </div>
+
      {:else if view.name === "activity"}
+
        {#each timelineTuple as [revision, timelines], index}
+
          {@const previousRevision =
+
            index > 0 ? patch.revisions[index - 1] : undefined}
+
          <RevisionComponent
+
            {baseUrl}
+
            projectId={project.id}
+
            {timelines}
+
            projectDefaultBranch={project.defaultBranch}
+
            projectHead={project.head}
+
            {...revision}
+
            first={index === 0}
+
            on:reply={createReply}
+
            patchId={patch.id}
+
            expanded={index === patch.revisions.length - 1}
+
            previousRevId={previousRevision?.id}
+
            previousRevOid={previousRevision?.oid} />
+
        {:else}
+
          <Placeholder emoji="🍂">
+
            <div slot="title">No activity</div>
+
            <div slot="body">No activity on this patch yet</div>
+
          </Placeholder>
+
        {/each}
+
      {:else if view.name === "commits"}
+
        <div class="commit-list">
+
          {#each view.commits as commit}
+
            <CommitTeaser projectId={project.id} {baseUrl} {commit} />
+
          {/each}
+
        </div>
+
      {:else if view.name === "files"}
+
        <div style:margin-top="1rem">
+
          <Changeset
+
            projectId={project.id}
+
            {baseUrl}
+
            revision={view.revision}
+
            diff={view.diff} />
+
        </div>
+
      {:else}
+
        {utils.unreachable(view.name)}
      {/if}
    </div>
-
    {#if view.name === "diff"}
-
      <div style:margin-top="1rem">
-
        <Changeset
-
          projectId={project.id}
-
          {baseUrl}
-
          revision={view.toCommit}
-
          diff={view.diff} />
-
      </div>
-
    {:else if view.name === "activity"}
-
      {#each timelineTuple as [revision, timelines], index}
-
        {@const previousRevision =
-
          index > 0 ? patch.revisions[index - 1] : undefined}
-
        <RevisionComponent
-
          {baseUrl}
-
          projectId={project.id}
-
          {timelines}
-
          projectDefaultBranch={project.defaultBranch}
-
          projectHead={project.head}
-
          {...revision}
-
          first={index === 0}
-
          on:reply={createReply}
-
          patchId={patch.id}
-
          expanded={index === patch.revisions.length - 1}
-
          previousRevId={previousRevision?.id}
-
          previousRevOid={previousRevision?.oid} />
-
      {:else}
-
        <Placeholder emoji="🍂">
-
          <div slot="title">No activity</div>
-
          <div slot="body">No activity on this patch yet</div>
-
        </Placeholder>
-
      {/each}
-
    {:else if view.name === "commits"}
-
      <div class="commit-list">
-
        {#each view.commits as commit}
-
          <CommitTeaser projectId={project.id} {baseUrl} {commit} />
-
        {/each}
-
      </div>
-
    {:else if view.name === "files"}
-
      <div style:margin-top="1rem">
-
        <Changeset
-
          projectId={project.id}
-
          {baseUrl}
-
          revision={view.revision}
-
          diff={view.diff} />
-
      </div>
-
    {:else}
-
      {utils.unreachable(view.name)}
-
    {/if}
-
  </div>

-
  <div class="metadata">
-
    <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"}
-
                <Icon size="small" name="checkmark" />
-
              {:else if review.verdict === "reject"}
-
                <Icon size="small" name="cross" />
-
              {:else}
-
                <Icon size="small" name="chat" />
-
              {/if}
-
            </span>
-
            <Authorship
-
              authorId={review.author.id}
-
              authorAlias={review.author.alias} />
-
          </div>
-
        {:else}
-
          <div class="txt-missing">No reviews</div>
-
        {/each}
+
    <div class="metadata">
+
      <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"}
+
                  <Icon size="small" name="checkmark" />
+
                {:else if review.verdict === "reject"}
+
                  <Icon size="small" name="cross" />
+
                {:else}
+
                  <Icon size="small" name="chat" />
+
                {/if}
+
              </span>
+
              <Authorship
+
                authorId={review.author.id}
+
                authorAlias={review.author.alias} />
+
            </div>
+
          {:else}
+
            <div class="txt-missing">No reviews</div>
+
          {/each}
+
        </div>
      </div>
+
      <LabelInput {action} labels={patch.labels} on:save={saveLabels} />
    </div>
-
    <LabelInput {action} labels={patch.labels} on:save={saveLabels} />
  </div>
-
</div>
+
</Layout>
modified src/views/projects/Patches.svelte
@@ -7,15 +7,16 @@

  import Button from "@app/components/Button.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
+
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import Loading from "@app/components/Loading.svelte";
  import PatchTeaser from "./Patch/PatchTeaser.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
  import SquareButton from "@app/components/SquareButton.svelte";

-
  export let project: Project;
  export let baseUrl: BaseUrl;
  export let patches: Patch[];
+
  export let project: Project;
  export let state: PatchState["status"];

  let loading = false;
@@ -95,65 +96,67 @@
  }
</style>

-
<div class="patches">
-
  <div style="margin-bottom: 2rem;">
-
    <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
-
      {#each options as option}
-
        {#if option.disabled}
-
          <SquareButton
-
            clickable={option.disabled}
-
            active={option.value === state}
-
            disabled={option.disabled}>
-
            {option.title}
-
          </SquareButton>
-
        {:else}
-
          <Link
-
            route={{
-
              resource: "project.patches",
-
              project: project.id,
-
              node: baseUrl,
-
              search: `state=${option.value}`,
-
            }}>
+
<Layout {baseUrl} {project} activeTab="patches">
+
  <div class="patches">
+
    <div style="margin-bottom: 2rem;">
+
      <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
+
        {#each options as option}
+
          {#if option.disabled}
            <SquareButton
              clickable={option.disabled}
              active={option.value === state}
              disabled={option.disabled}>
              {option.title}
            </SquareButton>
-
          </Link>
+
          {:else}
+
            <Link
+
              route={{
+
                resource: "project.patches",
+
                project: project.id,
+
                node: baseUrl,
+
                search: `state=${option.value}`,
+
              }}>
+
              <SquareButton
+
                clickable={option.disabled}
+
                active={option.value === state}
+
                disabled={option.disabled}>
+
                {option.title}
+
              </SquareButton>
+
            </Link>
+
          {/if}
+
        {/each}
+
      </div>
+
    </div>
+
    <div class="patches-list">
+
      {#each allPatches as patch (patch.id)}
+
        <div class="teaser">
+
          <PatchTeaser {baseUrl} projectId={project.id} {patch} />
+
        </div>
+
      {:else}
+
        {#if error}
+
          <ErrorMessage message="Couldn't load patches." stackTrace={error} />
+
        {:else if loading}
+
          <!-- We already show a loader below. -->
+
        {:else}
+
          <Placeholder emoji="🍂">
+
            <div slot="title">{capitalize(state)} patches</div>
+
            <div slot="body">No patches matched the current filter</div>
+
          </Placeholder>
        {/if}
      {/each}
    </div>
-
  </div>
-
  <div class="patches-list">
-
    {#each allPatches as patch (patch.id)}
-
      <div class="teaser">
-
        <PatchTeaser {baseUrl} projectId={project.id} {patch} />
-
      </div>
-
    {:else}
-
      {#if error}
-
        <ErrorMessage message="Couldn't load patches." stackTrace={error} />
-
      {:else if loading}
-
        <!-- We already show a loader below. -->
-
      {:else}
-
        <Placeholder emoji="🍂">
-
          <div slot="title">{capitalize(state)} patches</div>
-
          <div slot="body">No patches matched the current filter</div>
-
        </Placeholder>
+
    <div class="more">
+
      {#if loading}
+
        <div style:margin-top={page === 0 ? "8rem" : ""}>
+
          <Loading small={page !== 0} center />
+
        </div>
      {/if}
-
    {/each}
-
  </div>
-
  <div class="more">
-
    {#if loading}
-
      <div style:margin-top={page === 0 ? "8rem" : ""}>
-
        <Loading small={page !== 0} center />
-
      </div>
-
    {/if}

-
    {#if showMoreButton}
-
      <Button variant="foreground" on:click={() => loadMore(state)}>
-
        More
-
      </Button>
-
    {/if}
+
      {#if showMoreButton}
+
        <Button variant="foreground" on:click={() => loadMore(state)}>
+
          More
+
        </Button>
+
      {/if}
+
    </div>
  </div>
-
</div>
+
</Layout>
modified src/views/projects/View.svelte
@@ -6,77 +6,34 @@

  import Browser from "./Browser.svelte";
  import Commit from "./Commit.svelte";
-
  import Header from "./Header.svelte";
  import History from "./History.svelte";
  import Issue from "./Issue.svelte";
  import Issues from "./Issues.svelte";
  import NewIssue from "./Issue/New.svelte";
  import Patch from "./Patch.svelte";
  import Patches from "./Patches.svelte";
-
  import ProjectMeta from "./ProjectMeta.svelte";

  export let baseUrl: BaseUrl;
  export let project: Project;
  export let view: ProjectLoadedView;
-

-
  let peer: string | undefined;
-
  $: if (view.resource === "tree" || view.resource === "history") {
-
    peer = view.peer;
-
  } else {
-
    peer = undefined;
-
  }
</script>

-
<style>
-
  .header {
-
    width: 100%;
-
    max-width: var(--content-max-width);
-
    min-width: var(--content-min-width);
-
    padding-top: 4rem;
-
  }
-
  main {
-
    width: 100%;
-
    max-width: var(--content-max-width);
-
    min-width: var(--content-min-width);
-
    padding-bottom: 4rem;
-
  }
-
</style>
-

-
<div class="header">
-
  <ProjectMeta
-
    nodeId={peer}
-
    projectDescription={project.description}
-
    projectId={project.id}
-
    projectName={project.name}
-
    {baseUrl} />
-
  <Header
-
    openIssueCount={project.issues.open}
-
    openPatchCount={project.patches.open}
-
    projectId={project.id}
-
    projectName={project.name}
-
    resource={view.resource}
-
    trackings={project.trackings}
-
    {baseUrl} />
-
</div>
-

-
<main>
-
  {#if view.resource === "tree"}
-
    <Browser {...view} {baseUrl} {project} />
-
  {:else if view.resource === "history"}
-
    <History {...view} {baseUrl} {project} />
-
  {:else if view.resource === "commit"}
-
    <Commit {...view} {baseUrl} {project} />
-
  {:else if view.resource === "issues"}
-
    <Issues {...view} {baseUrl} {project} />
-
  {:else if view.resource === "newIssue"}
-
    <NewIssue {baseUrl} {project} />
-
  {:else if view.resource === "issue"}
-
    <Issue {...view} {baseUrl} {project} />
-
  {:else if view.resource === "patches"}
-
    <Patches {...view} {baseUrl} {project} />
-
  {:else if view.resource === "patch"}
-
    <Patch {...view} {baseUrl} {project} />
-
  {:else}
-
    {unreachable(view)}
-
  {/if}
-
</main>
+
{#if view.resource === "tree"}
+
  <Browser {...view} {baseUrl} {project} />
+
{:else if view.resource === "history"}
+
  <History {...view} {baseUrl} {project} />
+
{:else if view.resource === "commit"}
+
  <Commit {...view} {baseUrl} {project} />
+
{:else if view.resource === "issues"}
+
  <Issues {...view} {baseUrl} {project} />
+
{:else if view.resource === "newIssue"}
+
  <NewIssue {...view} {baseUrl} {project} />
+
{:else if view.resource === "issue"}
+
  <Issue {...view} {baseUrl} {project} />
+
{:else if view.resource === "patches"}
+
  <Patches {...view} {baseUrl} {project} />
+
{:else if view.resource === "patch"}
+
  <Patch {...view} {baseUrl} {project} />
+
{:else}
+
  {unreachable(view)}
+
{/if}