Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Rework releases list with teaser, filter, prominent CTA
Daniel Norman committed 7 days ago
commit 1bce824fa02c1fc5f4e6e003ec68593931d9cb7e
parent 034509f7fd1bb1de137546f793c9b57b228f206b
3 files changed +203 -67
modified crates/radicle-types/src/traits/release.rs
@@ -62,11 +62,9 @@ pub trait Releases: Profile {
        };

        let surf_repo = surf::Repository::open(repo.path())?;
-
        let tag_name = release.tag().and_then(|oid| {
-
            build_tag_index(&surf_repo)
-
                .get(&oid.to_string())
-
                .cloned()
-
        });
+
        let tag_name = release
+
            .tag()
+
            .and_then(|oid| build_tag_index(&surf_repo).get(&oid.to_string()).cloned());
        let commit_summary = commit_summary(&surf_repo, *release.oid());

        Ok(Some(release::Release::new(
added src/components/ReleaseTeaser.svelte
@@ -0,0 +1,136 @@
+
<script lang="ts">
+
  import type { Release } from "@bindings/cob/release/Release";
+

+
  import { push } from "@app/lib/router";
+
  import { authorForNodeId, formatTimestamp } from "@app/lib/utils";
+

+
  import Icon from "@app/components/Icon.svelte";
+
  import Id from "@app/components/Id.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+

+
  interface Props {
+
    release: Release;
+
    rid: string;
+
  }
+

+
  const { release, rid }: Props = $props();
+
</script>
+

+
<style>
+
  .teaser {
+
    display: flex;
+
    align-items: flex-start;
+
    justify-content: space-between;
+
    gap: 0.5rem;
+
    min-height: 5rem;
+
    background-color: var(--color-surface-canvas);
+
    padding: 1rem 1.25rem;
+
    cursor: pointer;
+
    font: var(--txt-body-l-regular);
+
    word-break: break-word;
+
    width: 100%;
+
  }
+
  .teaser:hover {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .teaser:first-of-type {
+
    border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
+
  }
+
  .teaser:last-of-type {
+
    border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
+
  }
+
  .teaser:only-of-type {
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .left {
+
    display: flex;
+
    align-items: flex-start;
+
    gap: 0.75rem;
+
  }
+
  .body {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
    align-items: flex-start;
+
  }
+
  .title {
+
    font: var(--txt-body-l-semibold);
+
    color: var(--color-text-primary);
+
  }
+
  .meta {
+
    display: flex;
+
    flex-wrap: wrap;
+
    align-items: center;
+
    gap: 0.375rem;
+
    color: var(--color-text-secondary);
+
    font: var(--txt-body-m-regular);
+
  }
+
  .tag-pill {
+
    font: var(--txt-body-s-mono);
+
    color: var(--color-text-secondary);
+
    background-color: var(--color-surface-subtle);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    padding: 0.0625rem 0.375rem;
+
  }
+
  .right {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
    white-space: nowrap;
+
  }
+
  .count-chip {
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    height: 1.5rem;
+
    padding: 0 0.5rem;
+
    color: var(--color-text-tertiary);
+
  }
+
</style>
+

+
<!-- svelte-ignore a11y_click_events_have_key_events -->
+
<div
+
  tabindex="0"
+
  role="button"
+
  class="teaser"
+
  onclick={() => {
+
    void push({
+
      resource: "repo.release",
+
      rid,
+
      release: release.id,
+
    });
+
  }}>
+
  <div class="left">
+
    <div class="global-chip" style:padding="0">
+
      <Icon name="commit" />
+
    </div>
+
    <div class="body">
+
      <span class="title">
+
        {release.tagName ??
+
          release.commitSummary ??
+
          `Release ${release.oid.slice(0, 7)}`}
+
      </span>
+
      <div class="meta">
+
        <NodeId {...authorForNodeId(release.creator)} />
+
        released
+
        <Id id={release.oid} clipboard={release.oid} />
+
        {#if release.tagName}
+
          <span class="tag-pill">{release.tagName}</span>
+
        {/if}
+
        {formatTimestamp(release.timestamp * 1000)}
+
      </div>
+
    </div>
+
  </div>
+

+
  <div class="right">
+
    <span class="count-chip">
+
      <Icon name="archive" />
+
      {release.artifacts.length}
+
    </span>
+
  </div>
+
</div>
modified src/views/repo/Releases.svelte
@@ -2,8 +2,14 @@
  import type { Release } from "@bindings/cob/release/Release";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

-
  import * as router from "@app/lib/router";
+
  import fuzzysort from "fuzzysort";

+
  import { modifierKey } from "@app/lib/utils";
+

+
  import Button from "@app/components/Button.svelte";
+
  import FuzzySearch from "@app/components/FuzzySearch.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import ReleaseTeaser from "@app/components/ReleaseTeaser.svelte";
  import ScrollArea from "@app/components/ScrollArea.svelte";
  import Topbar from "@app/components/Topbar.svelte";

@@ -18,14 +24,32 @@
  const { repo, releases }: Props = $props();

  let showNew = $state(false);
+
  let searchInput = $state("");
+
  let showSearch = $state(false);

-
  function shortOid(oid: string): string {
-
    return oid.slice(0, 7);
-
  }
+
  // Releases without artifacts are placeholders the user hasn't published
+
  // anything to yet — keep them off the list so it shows actual deliverables.
+
  const visibleReleases = $derived(
+
    releases.filter(r => r.artifacts.length > 0),
+
  );

-
  function formatTimestamp(ts: number): string {
-
    return new Date(ts * 1000).toLocaleString();
-
  }
+
  const searchable = $derived(
+
    visibleReleases.map(release => ({
+
      release,
+
      creator: release.creator.alias ?? "",
+
      tagName: release.tagName ?? "",
+
      summary: release.commitSummary ?? "",
+
      artifactNames: release.artifacts.map(a => a.name).join(" "),
+
    })),
+
  );
+

+
  const searchResults = $derived(
+
    fuzzysort.go(searchInput, searchable, {
+
      keys: ["release.oid", "tagName", "summary", "creator", "artifactNames"],
+
      threshold: 0.5,
+
      all: true,
+
    }),
+
  );
</script>

<style>
@@ -45,51 +69,28 @@
    gap: 1px;
    min-height: 100%;
  }
-
  .row {
-
    display: flex;
-
    align-items: center;
-
    padding: 0.75rem 1rem;
-
    background-color: var(--color-surface-1);
-
    text-decoration: none;
-
    color: inherit;
-
    gap: 1rem;
-
  }
-
  .row:hover {
-
    background-color: var(--color-surface-subtle);
-
  }
-
  .row .oid {
-
    font: var(--txt-body-m-mono);
-
    color: var(--color-text-secondary);
-
  }
-
  .row .count {
-
    font: var(--txt-body-s-regular);
-
    color: var(--color-text-secondary);
-
  }
-
  .row .timestamp {
-
    font: var(--txt-body-s-regular);
-
    color: var(--color-text-secondary);
-
    margin-left: auto;
-
  }
-
  .empty {
-
    display: flex;
-
    flex: 1;
-
    align-items: center;
-
    justify-content: center;
-
    color: var(--color-text-secondary);
-
    padding: 2rem;
-
  }
</style>

<Layout selfScroll>
  <div class="page">
    <Topbar>
      <span class="topbar-title">Releases</span>
-
      <div style:margin-left="auto">
-
        <button
+
      <div class="global-flex" style:margin-left="auto" style:gap="0.5rem">
+
        <FuzzySearch
+
          hasItems={visibleReleases.length > 0}
+
          placeholder={`Fuzzy filter releases ${modifierKey()} + f`}
+
          bind:show={showSearch}
+
          bind:value={searchInput} />
+
        <Button
+
          variant="secondary"
+
          styleHeight="2rem"
          onclick={() => (showNew = !showNew)}
-
          style="padding: 0.25rem 0.5rem; border-radius: var(--border-radius-sm); border: 1px solid var(--color-border-subtle); background-color: var(--color-surface-subtle); cursor: pointer;">
-
          {showNew ? "Cancel" : "New release"}
-
        </button>
+
          active={showNew}>
+
          <Icon name={showNew ? "close" : "plus"} />
+
          <span class="global-hide-on-small-desktop-down">
+
            {showNew ? "Cancel" : "New release"}
+
          </span>
+
        </Button>
      </div>
    </Topbar>

@@ -101,24 +102,25 @@

    <ScrollArea style="height: 100%; min-width: 0;">
      <div class="list">
-
        {#each releases as release (release.id)}
-
          <a
-
            class="row"
-
            href={router.routeToPath({
-
              resource: "repo.release",
-
              rid: repo.rid,
-
              release: release.id,
-
            })}>
-
            <span class="oid">{shortOid(release.oid)}</span>
-
            <span class="count">
-
              {release.artifacts.length}
-
              {release.artifacts.length === 1 ? "artifact" : "artifacts"}
-
            </span>
-
            <span class="timestamp">{formatTimestamp(release.timestamp)}</span>
-
          </a>
-
        {:else}
-
          <div class="empty">No releases yet</div>
+
        {#each searchResults as result (result.obj.release.id)}
+
          <ReleaseTeaser release={result.obj.release} rid={repo.rid} />
        {/each}
+

+
        {#if searchResults.length === 0}
+
          <div
+
            class="global-flex"
+
            style:flex="1"
+
            style:justify-content="center"
+
            style:align-items="center">
+
            <div class="txt-missing txt-body-m-regular">
+
              {#if visibleReleases.length > 0}
+
                No matching releases
+
              {:else}
+
                No releases yet
+
              {/if}
+
            </div>
+
          </div>
+
        {/if}
      </div>
    </ScrollArea>
  </div>