Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add Get started page and header CTA
Brandon Oxendine committed 21 days ago
commit a2add707c89b2f2b49bfda3daa9c718814adae08
parent ad8b0699b4d798022f5198f60890ce0586a438fd
12 files changed +614 -2
added public/images/agent-skill.png
added public/images/ci.png
added public/images/cli.png
added public/images/desktop.png
added public/images/garden.png
modified src/App.svelte
@@ -14,6 +14,7 @@
  import LoadingBar from "./App/LoadingBar.svelte";

  import Commit from "@app/views/repos/Commit.svelte";
+
  import GetStarted from "@app/views/getStarted/View.svelte";
  import History from "@app/views/repos/History.svelte";
  import Issue from "@app/views/repos/Issue.svelte";
  import Issues from "@app/views/repos/Issues.svelte";
@@ -71,6 +72,8 @@
  </div>
{:else if $activeRouteStore.resource === "nodes"}
  <Nodes {...$activeRouteStore.params} />
+
{:else if $activeRouteStore.resource === "getStarted"}
+
  <GetStarted />
{:else if $activeRouteStore.resource === "users"}
  <Users {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.source"}
modified src/components/Button.svelte
@@ -163,7 +163,7 @@

  .secondary {
    color: var(--color-text-on-brand);
-
    background-color: var(--color-surface-brand-primary);
+
    background-color: var(--color-surface-brand-secondary);
  }

  .secondary[disabled] {
@@ -172,7 +172,7 @@
  }

  .secondary:not([disabled]):hover {
-
    background-color: var(--color-surface-brand-secondary);
+
    background-color: var(--color-surface-brand-primary);
  }

  .secondary-mobile {
added src/components/CopyCommand.svelte
@@ -0,0 +1,82 @@
+
<script lang="ts">
+
  import { onDestroy } from "svelte";
+
  import Icon from "@app/components/Icon.svelte";
+

+
  export let command: string = "curl -sSf https://radicle.xyz/install | sh";
+

+
  let isCopied = false;
+
  let resetCopyStateTimeout: ReturnType<typeof setTimeout> | undefined;
+

+
  async function copyToClipboard() {
+
    try {
+
      await navigator.clipboard.writeText(command);
+
      isCopied = true;
+
      if (resetCopyStateTimeout) clearTimeout(resetCopyStateTimeout);
+
      resetCopyStateTimeout = setTimeout(() => {
+
        isCopied = false;
+
      }, 2000);
+
    } catch (err) {
+
      console.error("Failed to copy to clipboard:", err);
+
    }
+
  }
+

+
  onDestroy(() => {
+
    if (resetCopyStateTimeout) clearTimeout(resetCopyStateTimeout);
+
  });
+
</script>
+

+
<style>
+
  .wrapper {
+
    display: block;
+
    overflow: hidden;
+
  }
+

+
  .codeblock {
+
    display: inline-flex;
+
    align-items: center;
+
    padding: 0.5rem 0.5rem 0.5rem 0.75rem;
+
    gap: 1.5rem;
+
    background: var(--color-surface-subtle);
+
    border-radius: var(--border-radius-sm);
+
    max-width: 100%;
+
    box-sizing: border-box;
+
  }
+

+
  code {
+
    font: var(--txt-code-regular);
+
    color: var(--color-text-secondary);
+
    white-space: nowrap;
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
  }
+

+
  .copy-button {
+
    display: flex;
+
    justify-content: center;
+
    align-items: center;
+
    padding: 0;
+
    flex-shrink: 0;
+
    width: 1rem;
+
    height: 1rem;
+
    background: none;
+
    border: none;
+
    cursor: pointer;
+
    color: var(--color-text-secondary);
+
  }
+

+
  .copy-button:hover {
+
    color: var(--color-text-primary);
+
  }
+
</style>
+

+
<span class="wrapper">
+
  <span class="codeblock">
+
    <code>{command}</code>
+
    <button
+
      class="copy-button"
+
      aria-label={isCopied ? "Copied to clipboard" : "Copy to clipboard"}
+
      on:click={copyToClipboard}>
+
      <Icon name={isCopied ? "checkmark" : "copy"} />
+
    </button>
+
  </span>
+
</span>
modified src/components/Header.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
  import Settings from "@app/App/Settings.svelte";
  import Help from "@app/App/Help.svelte";
+
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
  import IconButton from "@app/components/IconButton.svelte";
  import Link from "@app/components/Link.svelte";
@@ -28,6 +29,13 @@
  .right-section {
    display: flex;
    justify-content: flex-end;
+
    align-items: center;
+
    gap: 0.75rem;
+
  }
+
  @media (max-width: 719.98px) {
+
    .get-started {
+
      display: none;
+
    }
  }
  .divider {
    height: 1px;
@@ -50,6 +58,14 @@
  </div>

  <div class="right-section">
+
    <span class="get-started">
+
      <Link
+
        route={{ resource: "getStarted" }}
+
        ariaLabel="Get started with Radicle">
+
        <Button variant="secondary">Get started with Radicle</Button>
+
      </Link>
+
    </span>
+

    <Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
      <IconButton
        slot="toggle"
modified src/lib/router.ts
@@ -115,6 +115,9 @@ function setTitle(loadedRoute: LoadedRoute) {
    title.push("Radicle");
  } else if (loadedRoute.resource === "users") {
    title.push(...userTitle(loadedRoute));
+
  } else if (loadedRoute.resource === "getStarted") {
+
    title.push("Get started");
+
    title.push("Radicle");
  } else if (loadedRoute.resource === "notFound") {
    title.push("Page not found");
    title.push("Radicle");
@@ -177,6 +180,9 @@ function urlToRoute(url: URL): Route | null {

  const resource = segments.shift();
  switch (resource) {
+
    case "get-started": {
+
      return { resource: "getStarted" };
+
    }
    case "nodes":
    case "seeds": {
      const hostAndPort = segments.shift();
@@ -220,6 +226,8 @@ export function routeToPath(route: Route): string {
    } else {
      return nodePath(route.params.baseUrl);
    }
+
  } else if (route.resource === "getStarted") {
+
    return "/get-started";
  } else if (route.resource === "users") {
    return userRouteToPath(route);
  } else if (
modified src/lib/router/definitions.ts
@@ -17,6 +17,10 @@ interface BootingRoute {
  resource: "booting";
}

+
export interface GetStartedRoute {
+
  resource: "getStarted";
+
}
+

export interface NotFoundRoute {
  resource: "notFound";
  params: { title: string; description?: string; baseUrl?: BaseUrl };
@@ -36,6 +40,7 @@ export interface ErrorRoute {

export type Route =
  | BootingRoute
+
  | GetStartedRoute
  | UserRoute
  | ErrorRoute
  | NotFoundRoute
@@ -44,6 +49,7 @@ export type Route =

export type LoadedRoute =
  | BootingRoute
+
  | GetStartedRoute
  | UserLoadedRoute
  | ErrorRoute
  | NotFoundRoute
added src/views/getStarted/View.svelte
@@ -0,0 +1,497 @@
+
<script lang="ts">
+
  import { slide } from "svelte/transition";
+

+
  import CopyCommand from "@app/components/CopyCommand.svelte";
+
  import Header from "@app/components/Header.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+

+
  let showDownloads = false;
+
  let openItem = 0;
+

+
  const downloads = [
+
    {
+
      name: "Apple Silicon",
+
      description:
+
        "Run the command in your terminal to download and open the DMG file. Then drag the Radicle app to your Applications folder.",
+
      command:
+
        "curl --output ~/Downloads/radicle-desktop-aarch64.dmg https://files.radicle.xyz/releases/radicle-desktop/latest/radicle-desktop-aarch64.dmg && open ~/Downloads/radicle-desktop-aarch64.dmg",
+
    },
+
    {
+
      name: "Linux AppImage",
+
      description:
+
        "Download, make the file executable with chmod +x, and run it.",
+
      command:
+
        "curl --output radicle-desktop-amd64.AppImage https://files.radicle.xyz/releases/radicle-desktop/latest/radicle-desktop-amd64.AppImage",
+
    },
+
    {
+
      name: "Debian / Ubuntu",
+
      description: "Install the keyring, add the APT repository, then install.",
+
      command:
+
        'curl -LO https://radicle.xyz/apt/radicle-archive-keyring.deb && chmod a+r radicle-archive-keyring.deb && sudo apt install ./radicle-archive-keyring.deb && echo "deb [signed-by=/usr/share/radicle/radicle-archive-keyring.asc] https://radicle.xyz/apt release main" | sudo tee /etc/apt/sources.list.d/radicle.list && sudo apt update && sudo apt install radicle-desktop',
+
    },
+
    {
+
      name: "Arch Linux",
+
      description: "Available from the Arch User Repository.",
+
      command: "yay -S radicle-desktop",
+
    },
+
    {
+
      name: "NixOS",
+
      description:
+
        "Give it a try with nix run and if you like it, make it permanent with nix profile install.",
+
      command:
+
        "nix run 'git+https://seed.radicle.xyz/z4D5UCArafTzTQpDZNQRuqswh3ury.git'",
+
    },
+
    {
+
      name: "Windows WSL2",
+
      description:
+
        "Use WSL2 with any of the Linux install options to run Radicle Desktop on Windows.",
+
      command: "",
+
    },
+
  ];
+

+
  function toggleItem(index: number) {
+
    openItem = openItem === index ? -1 : index;
+
  }
+

+
  function handleDesktopAction() {
+
    if (window.matchMedia("(max-width: 64rem)").matches) {
+
      const installUrl = `${window.location.origin}/get-started`;
+
      const subject = encodeURIComponent("Install Radicle Desktop");
+
      const body = encodeURIComponent(
+
        `Open this page on your desktop:\n\n${installUrl}`,
+
      );
+
      window.location.href = `mailto:?subject=${subject}&body=${body}`;
+
      return;
+
    }
+

+
    showDownloads = !showDownloads;
+
  }
+
</script>
+

+
<style>
+
  .page {
+
    --install-inline-gap: 4rem;
+
    padding: 2.5rem var(--install-inline-gap) 4rem;
+
    max-width: 90rem;
+
    margin: 0 auto;
+
  }
+

+
  .page-header h1 {
+
    margin: 0;
+
    max-width: 50rem;
+
  }
+

+
  .tertiary {
+
    color: var(--color-text-tertiary);
+
  }
+

+
  .card-row {
+
    display: grid;
+
    grid-template-columns: 1fr 1fr;
+
    gap: var(--install-inline-gap);
+
  }
+

+
  .top-row {
+
    padding-top: var(--install-inline-gap);
+
  }
+

+
  .bottom-row {
+
    padding-top: var(--install-inline-gap);
+
  }
+

+
  .product-card {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1.5rem;
+
    margin: 0;
+
  }
+

+
  .product-media {
+
    width: 100%;
+
    height: auto;
+
    border-radius: var(--border-radius-sm);
+
    background: var(--color-surface-subtle);
+
    aspect-ratio: 16 / 10;
+
    object-fit: cover;
+
  }
+

+
  .product-card-text {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.75rem;
+
    align-items: flex-start;
+
  }
+

+
  .product-title {
+
    margin: 0;
+
  }
+

+
  .product-description {
+
    margin: 0;
+
    color: var(--color-text-tertiary);
+
    max-width: 32rem;
+
  }
+

+
  .product-link {
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    background: none;
+
    border: none;
+
    padding: 0;
+
    cursor: pointer;
+
    color: var(--color-text-primary);
+
    text-decoration: none;
+
  }
+

+
  .product-link:hover {
+
    color: var(--color-brand-hover);
+
  }
+

+
  .inline-link {
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    color: var(--color-text-primary);
+
    text-decoration: none;
+
    font: var(--txt-body-l-medium);
+
    vertical-align: bottom;
+
  }
+

+
  .inline-link:hover {
+
    color: var(--color-brand-hover);
+
  }
+

+
  .download-panel {
+
    display: flex;
+
    flex-direction: row;
+
    align-items: flex-start;
+
    gap: 1.5rem;
+
    padding-right: 1.5rem;
+
    overflow: hidden;
+
  }
+

+
  .download-left {
+
    width: 20rem;
+
    flex-shrink: 0;
+
    padding: 1.25rem 1.5rem 1.25rem 0;
+
  }
+

+
  .download-left h3 {
+
    margin: 0;
+
  }
+

+
  .download-right {
+
    display: flex;
+
    flex-direction: column;
+
    flex-grow: 1;
+
    min-width: 0;
+
  }
+

+
  .download-divider {
+
    height: 1px;
+
    background: var(--color-border-subtle);
+
  }
+

+
  .download-item {
+
    border-radius: var(--border-radius-sm);
+
    min-width: 0;
+
  }
+

+
  .download-question {
+
    display: flex;
+
    flex-direction: row;
+
    align-items: center;
+
    justify-content: space-between;
+
    padding: 1.25rem 0;
+
    gap: 2rem;
+
    width: 100%;
+
    background: none;
+
    border: none;
+
    cursor: pointer;
+
    color: var(--color-text-primary);
+
    text-align: left;
+
  }
+

+
  .download-question:hover {
+
    color: var(--color-brand-hover);
+
  }
+

+
  .chevron {
+
    flex: none;
+
    color: var(--color-text-tertiary);
+
    transition: transform 0.2s ease;
+
  }
+

+
  .chevron.open {
+
    transform: rotate(180deg);
+
  }
+

+
  .download-answer {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1.5rem;
+
    padding-bottom: 2.5rem;
+
    min-width: 0;
+
  }
+

+
  .download-description {
+
    color: var(--color-text-tertiary);
+
    max-width: 45rem;
+
    margin: 0;
+
  }
+

+
  .download-label-desktop,
+
  .download-label-mobile {
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
  }
+

+
  .download-label-mobile {
+
    display: none;
+
  }
+

+
  .garden-banner {
+
    display: flex;
+
    flex-direction: column;
+
    justify-content: space-between;
+
    margin-top: var(--install-inline-gap);
+
    padding: 1.5rem;
+
    min-height: 25.5rem;
+
    background:
+
      url("/images/garden.png") no-repeat right center,
+
      var(--color-accent-citrus-500);
+
    background-size: cover;
+
    border-radius: var(--border-radius-sm);
+
  }
+

+
  .garden-title {
+
    margin: 0;
+
    max-width: 28rem;
+
    color: #00060f;
+
  }
+

+
  .garden-bottom {
+
    display: flex;
+
    flex-direction: row;
+
    justify-content: space-between;
+
    align-items: flex-end;
+
    gap: 1rem;
+
  }
+

+
  .garden-description {
+
    margin: 0;
+
    max-width: 25rem;
+
    color: #00060f;
+
  }
+

+
  .garden-button {
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    padding: 0.375rem 0.75rem;
+
    background: #ffffff;
+
    border-radius: var(--border-radius-sm);
+
    color: #00060f;
+
    text-decoration: none;
+
    white-space: nowrap;
+
  }
+

+
  .garden-button:hover {
+
    opacity: 0.85;
+
  }
+

+
  @media (max-width: 90rem) {
+
    .page {
+
      --install-inline-gap: 2rem;
+
    }
+
  }
+

+
  @media (max-width: 64rem) {
+
    .page {
+
      --install-inline-gap: 1.5rem;
+
    }
+

+
    .card-row {
+
      grid-template-columns: 1fr;
+
    }
+

+
    .download-label-desktop {
+
      display: none;
+
    }
+

+
    .download-label-mobile {
+
      display: inline-flex;
+
      align-items: center;
+
      gap: 0.25rem;
+
    }
+

+
    .download-panel {
+
      flex-direction: column;
+
      padding-right: 0;
+
    }
+

+
    .download-left {
+
      width: 100%;
+
      padding-right: 0;
+
      padding-bottom: 0;
+
    }
+

+
    .garden-banner {
+
      min-height: auto;
+
    }
+

+
    .garden-bottom {
+
      flex-direction: column;
+
      align-items: flex-start;
+
    }
+
  }
+
</style>
+

+
<Header />
+

+
<main class="page">
+
  <header class="page-header">
+
    <h1 class="txt-heading-xxl">
+
      Get started with Radicle. <span class="tertiary">
+
        Everything you need to collaborate on code, from the terminal, the
+
        desktop, or your CI pipeline.
+
      </span>
+
    </h1>
+
  </header>
+

+
  <div class="card-row top-row">
+
    <article id="cli" class="product-card">
+
      <img class="product-media" src="/images/cli.png" alt="CLI" />
+
      <div class="product-card-text">
+
        <h3 class="product-title txt-heading-l">CLI</h3>
+
        <p class="product-description txt-body-l-regular">
+
          Work directly from your terminal to manage code, issues, patches, CI
+
          or even your sovereign identity. <a
+
            href="https://radicle.dev/guides/user"
+
            target="_blank"
+
            rel="noopener noreferrer"
+
            class="inline-link">Read the user guide</a>.
+
        </p>
+
        <CopyCommand />
+
      </div>
+
    </article>
+

+
    <article id="desktop" class="product-card">
+
      <img class="product-media" src="/images/desktop.png" alt="Desktop" />
+
      <div class="product-card-text">
+
        <h3 class="product-title txt-heading-l">Desktop</h3>
+
        <p class="product-description txt-body-l-regular">
+
          Collaborate through a visual interface with patches, issues, CI and
+
          built-in notifications. Your local-first forge, directly on your
+
          computer.
+
        </p>
+
        <button
+
          class="product-link txt-body-l-medium"
+
          on:click={handleDesktopAction}>
+
          <span class="download-label-desktop">
+
            Download <Icon name="arrow-down" />
+
          </span>
+
          <span class="download-label-mobile">
+
            Send to my desktop <Icon name="open-external" />
+
          </span>
+
        </button>
+
      </div>
+
    </article>
+
  </div>
+

+
  {#if showDownloads}
+
    <section class="download-panel" transition:slide={{ duration: 300 }}>
+
      <div class="download-left">
+
        <h3 class="txt-heading-l">Select your OS and follow the instructions</h3>
+
      </div>
+
      <div class="download-right">
+
        {#each downloads as dl, i}
+
          {#if i > 0}
+
            <div class="download-divider"></div>
+
          {/if}
+
          <div class="download-item">
+
            <button class="download-question" on:click={() => toggleItem(i)}>
+
              <span class="txt-body-l-semibold">{dl.name}</span>
+
              <span class="chevron" class:open={openItem === i}>
+
                <Icon name="chevron-up" />
+
              </span>
+
            </button>
+
            {#if openItem === i}
+
              <div class="download-answer">
+
                <p class="download-description txt-body-l-regular">
+
                  {dl.description}
+
                </p>
+
                {#if dl.command}
+
                  <CopyCommand command={dl.command} />
+
                {/if}
+
              </div>
+
            {/if}
+
          </div>
+
        {/each}
+
      </div>
+
    </section>
+
  {/if}
+

+
  <div class="card-row bottom-row">
+
    <article id="radicle-ci" class="product-card">
+
      <img class="product-media" src="/images/ci.png" alt="Radicle CI" />
+
      <div class="product-card-text">
+
        <h3 class="product-title txt-heading-l">Radicle CI</h3>
+
        <p class="product-description txt-body-l-regular">
+
          Automate builds and tests when patches land. Results are stored as
+
          collaborative objects, so they replicate with everything else.
+
        </p>
+
        <a
+
          href="https://radicle.dev/2025/07/23/using-radicle-ci-for-development"
+
          target="_blank"
+
          rel="noopener noreferrer"
+
          class="product-link txt-body-l-medium">
+
          Read the guide <Icon name="arrow-right" />
+
        </a>
+
      </div>
+
    </article>
+

+
    <article class="product-card">
+
      <img
+
        class="product-media"
+
        src="/images/agent-skill.png"
+
        alt="AI agent integration" />
+
      <div class="product-card-text">
+
        <h3 class="product-title txt-heading-l">AI agents</h3>
+
        <p class="product-description txt-body-l-regular">
+
          Teach your AI agent to use Radicle. Break down issues into tasks, sync
+
          progress, and capture session context.
+
        </p>
+
        <a
+
          href="https://app.radicle.xyz/nodes/seed.radicle.garden/rad:zvBj4kByGeQSrSy2c4H7fyK42cS8"
+
          target="_blank"
+
          rel="noopener noreferrer"
+
          class="product-link txt-body-l-medium">
+
          View repo <Icon name="open-external" />
+
        </a>
+
      </div>
+
    </article>
+
  </div>
+

+
  <section class="garden-banner">
+
    <h2 class="garden-title txt-heading-xxl">
+
      Introducing Radicle Garden. The quickest way to get started on the
+
      network.
+
    </h2>
+
    <div class="garden-bottom">
+
      <p class="garden-description txt-body-l-regular">
+
        Get started in minutes—no servers required. Instantly share code and
+
        discover new projects.
+
      </p>
+
      <a
+
        href="https://radicle.garden"
+
        target="_blank"
+
        rel="noopener noreferrer"
+
        class="garden-button txt-body-m-semibold">
+
        Visit radicle.garden <Icon name="open-external" />
+
      </a>
+
    </div>
+
  </section>
+
</main>