Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add Responsive Layout
Sebastian Martinez committed 4 years ago
commit 04ec3319e4166b2e88c58821a87a451326f976b3
parent a2a99140b8133c84ae70dec8ccf86fc1904a336c
21 files changed +482 -61
modified public/index.css
@@ -79,7 +79,7 @@ body {
	font-size: 16px;
	font-weight: 400;
	line-height: 1.6;
-
	min-width: 580px;
+
	min-width: var(--content-min-width);
	height: 100%;
	margin: 0;
	padding: 0;
@@ -92,6 +92,12 @@ body {
	scrollbar-color: var(--color-scrollbar) transparent;
}

+
@media (max-width: 720px) {
+
  body {
+
    min-width: 0;
+
  }
+
}
+

html {
	height: 100%;
	-webkit-text-size-adjust: 100%;
@@ -217,7 +223,7 @@ button.small {
	min-width: 6rem;
}
button.tiny {
-
	min-width: 0;
+
	min-width: auto;
	padding: 0 0.75rem;
	height: 2rem;
}
modified src/Address.svelte
@@ -16,9 +16,6 @@
  // This property allows components eg. Header.svelte to pass a resolved profile object.
  export let profile: Profile | null = null;

-
  let checksumAddress = compact
-
    ? formatAddress(address)
-
    : ethers.utils.getAddress(address);
  let addressType: AddressType | null = null;

  const nameOrAddress = profile?.name || address;
@@ -30,6 +27,9 @@
    }
  });
  $: addressLabel = profile?.name ? compact ? parseEnsLabel(profile.name, config) : profile.name : checksumAddress;
+
  $: checksumAddress = compact
+
    ? formatAddress(address)
+
    : ethers.utils.getAddress(address);
</script>

<style>
@@ -63,7 +63,7 @@
    <a href={explorerLink(address, config)} target="_blank">{addressLabel}</a>
    <span class="badge">contract</span>
  {:else if addressType === AddressType.EOA}
-
    <a href={`/users/${nameOrAddress}`}>{addressLabel}</a>
+
    <a use:link href={`/users/${nameOrAddress}`}>{addressLabel}</a>
  {:else} <!-- While we're waiting to find out what address type it is -->
    <a href={explorerLink(address, config)} target="_blank">{addressLabel}</a>
  {/if}
modified src/Card.svelte
@@ -54,6 +54,12 @@
    font-size: 0.875rem;
    color: var(--color-foreground-faded);
  }
+

+
  @media (max-width: 720px) {
+
    .card {
+
      margin-right: 0;
+
    }
+
  }
</style>

<Link to={path}>
modified src/Cards.svelte
@@ -28,6 +28,11 @@
    flex-direction: row;
    flex-wrap: wrap;
  }
+
  @media (max-width: 720px) {
+
    .list {
+
      justify-content: center;
+
    }
+
  }
</style>

  <div class="list">
modified src/Form.svelte
@@ -34,9 +34,10 @@
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  import marked from 'marked';
-
  import { capitalize, isUrl, isAddress } from '@app/utils';
+
  import { capitalize, isUrl, isAddress, formatSeedId } from '@app/utils';
  import Address from '@app/Address.svelte';
  import type { Config } from '@app/config';
+
  import Icon from "@app/Icon.svelte";

  export let fields: Field[];
  export let editable = false;
@@ -82,7 +83,7 @@
<style>
  .fields {
    display: grid;
-
    grid-template-columns: auto auto;
+
    grid-template-columns: 6rem auto;
    grid-gap: 1rem 1.5rem;
  }
  .fields > div {
@@ -145,6 +146,24 @@
  .actions.editable {
    visibility: visible;
  }
+
  .mobile {
+
    display: none !important;
+
  }
+
  .desktop {
+
    display: block !important;
+
  }
+

+
  @media (max-width: 720px) {
+
    .mobile {
+
      display: block !important;
+
    }
+
    .desktop {
+
      display: none !important;
+
    }
+
    .field {
+
      width: unset;
+
    }
+
  }
</style>

<div class="fields">
@@ -160,11 +179,26 @@
        <span class="field">
          {#if field.value}
            {#if isUrl(field.value)}
-
              <span>
+
              <span class="desktop">
                <a class="link" href="{field.value}" target="_blank">{field.value}</a>
              </span>
+
              <span class="mobile" title={field.value}>
+
                <Icon name="url" />
+
              </span>
            {:else if isAddress(field.value)}
-
              <Address resolve={field.resolve ?? false} address={field.value} {config} />
+
              <div class="desktop">
+
                <Address resolve={field.resolve ?? false} address={field.value} {config} />
+
              </div>
+
              <div class="mobile">
+
                <Address compact resolve={field.resolve ?? false} address={field.value} {config} />
+
              </div>
+
            {:else if (field.label === "Seed ID")}
+
              <div class="mobile">
+
                {formatSeedId(field.value)}
+
              </div>
+
              <div class="desktop">
+
                {field.value}
+
              </div>
            {:else}
              {field.value}
            {/if}
modified src/Header.svelte
@@ -11,12 +11,19 @@
  import { Profile, ProfileType } from "@app/profile";
  import Avatar from '@app/Avatar.svelte';
  import Search from '@app/Search.svelte';
+
  import Icon from "./Icon.svelte";
+
  import MobileNavbar from "./MobileNavbar.svelte";

  export let session: Session | null;
  export let config: Config;

  let sessionButton: HTMLElement | null = null;
  let sessionButtonHover = false;
+
  let mobileNavbarDisplayed = false;
+

+
  function toggleNavbar() {
+
    mobileNavbarDisplayed = !mobileNavbarDisplayed;
+
  }

  $: address = session && session.address;
  $: tokenBalance = session && session.tokenBalance;
@@ -107,6 +114,16 @@
    margin-left: 2rem;
    white-space: nowrap;
  }
+
  div.toggle {
+
    display: flex;
+
    justify-content: center;
+
    align-items: center;
+
    margin-left: 10px;
+
    height: 42px;
+
    width: 42px;
+
    z-index: 2;
+
    cursor: pointer;
+
  }

  @media(max-width: 800px) {
    .balance {
@@ -114,7 +131,7 @@
    }
  }
  @media(max-width: 720px) {
-
    .network {
+
    .network, .search, header .nav, .balance {
      display: none;
    }
  }
@@ -184,5 +201,12 @@
        <Connect className="small" {config} />
      </span>
    {/if}
+
    <div class="ellipsis toggle mobile" on:click={toggleNavbar}>
+
      <Icon name="ellipsis" width={27} height={27} />
+
    </div>
  </div>
+

+
  {#if mobileNavbarDisplayed}
+
    <MobileNavbar on:select={toggleNavbar} />
+
  {/if}
</header>
modified src/Icon.svelte
@@ -14,6 +14,16 @@
      name: "github",
      size: 24,
      data: `<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>`
+
    },
+
    {
+
      name: "url",
+
      size: 24,
+
      data: `<path d="m 22.149137,10.881006 -4.50822,4.506674 c -2.488332,2.489875 -6.525018,2.489875 -9.0133491,0 C 8.2354124,14.997069 7.9288596,14.55666 7.6596044,14.102258 l 2.0946322,-2.094633 c 0.099595,-0.100392 0.2225451,-0.157959 0.3400174,-0.225634 0.144712,0.494888 0.397583,0.963282 0.786651,1.352302 1.242598,1.243393 3.264874,1.241849 4.506675,0 l 4.506675,-4.5066751 c 1.243394,-1.2433942 1.243394,-3.2648741 0,-4.5074715 -1.241851,-1.2425972 -3.26328,-1.2425972 -4.506675,0 L 13.784698,5.7245722 C 12.484535,5.2180323 11.083184,5.0818859 9.7191796,5.2818228 L 13.134194,1.8668091 c 2.489875,-2.4890788 6.525017,-2.4890788 9.014892,0 2.488384,2.4891784 2.488384,6.5251176 5.1e-5,9.0141969 z m -11.917939,7.410517 -1.6036301,1.604425 c -1.2425972,1.241852 -3.2648738,1.241852 -4.5074712,0 -1.2425974,-1.243393 -1.2425974,-3.264875 0,-4.508218 l 4.5074712,-4.506675 c 1.2433943,-1.2433946 3.2640781,-1.2433946 4.5066761,0 0.388269,0.388272 0.641141,0.856666 0.787447,1.350757 0.118269,-0.06847 0.239676,-0.124494 0.339271,-0.224088 L 16.355594,9.9138881 C 16.087933,9.4579417 15.779785,9.0190766 15.38763,8.6277177 c -2.488331,-2.4890788 -6.5250181,-2.4890788 -9.0141462,0 L 1.8668091,13.134392 c -2.4890788,2.489875 -2.4890788,6.525018 0,9.014894 2.489079,2.488332 6.5250179,2.488332 9.0141469,0 l 3.415809,-3.415811 c -1.364801,0.200735 -2.766151,0.0638 -4.065567,-0.441952 z"/>`
+
    },
+
    {
+
      name: "ellipsis",
+
      size: 24,
+
      data: `<path d="M7 12a2 2 0 1 1-4.001-.001A2 2 0 0 1 7 12zm12-2a2 2 0 1 0 .001 4.001A2 2 0 0 0 19 10zm-7 0a2 2 0 1 0 .001 4.001A2 2 0 0 0 12 10z"/>`
    }
  ];
  const svg = icons.find(e => e.name === name);
added src/MobileNavbar.svelte
@@ -0,0 +1,78 @@
+
<script lang="ts">
+
  import { link } from 'svelte-routing';
+
  import { createEventDispatcher } from 'svelte';
+
  import Search from './Search.svelte';
+
  import { clickOutside } from "@app/utils";
+

+
  const dispatch = createEventDispatcher();
+

+
  function handleClickOutside() {
+
    dispatch("select");
+
  }
+
</script>
+

+
<style>
+
  .modal-floating {
+
    position: absolute;
+
    top: 0;
+
    left: 0;
+
    width: 100%;
+
    height: 100%;
+
    overflow: hidden;
+
  }
+
  .modal-floating {
+
    z-index: 300;
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
    background-color: #000000BF;
+
  }
+
  .modal {
+
    position:absolute;
+
    top: 90px;
+
    right: 1.5rem;
+
    padding: 1.5rem;
+
    font-family: var(--font-family-sans-serif);
+
    background: var(--color-background);
+
    min-width: 240px;
+
    max-width: 360px;
+
    border-radius: 1rem;
+
    text-align: center;
+
  }
+
  .modal-title {
+
    color: var(--color-foreground);
+
    font-size: 1rem;
+
    line-height: 2rem;
+
    text-align: left;
+
    text-overflow: ellipsis;
+
    overflow: hidden;
+
  }
+
  .modal-title a {
+
    color: var(--color-foreground-6);
+
    padding-left: 1.5rem;
+
  }
+
  .modal-title a:hover {
+
    color: var(--color-foreground);
+
  }
+
  .modal-title a:first-child {
+
    padding-left: 0.5rem;
+
  }
+
</style>
+

+
<div class="modal-floating">
+
  <div use:clickOutside={handleClickOutside} class="modal">
+
    <div class="modal-title">
+
      <div style="padding-bottom: 1rem;">
+
        <Search size={20} on:search={() => dispatch("select")} />
+
      </div>
+
      <div>
+
        <a use:link on:click={() => dispatch("select")} href="/orgs">
+
          Orgs
+
        </a>
+
        <a use:link on:click={() => dispatch("select")} href="/registrations">
+
          Register
+
        </a>
+
      </div>
+
    </div>
+
  </div>
+
</div>
modified src/Modal.svelte
@@ -91,6 +91,12 @@
  .modal-small .modal-subtitle {
    color: var(--color-foreground);
  }
+
  @media (max-width: 720px) {
+
    .modal {
+
      width: 90%;
+
      min-width: unset;
+
    }
+
  }
</style>

{#if floating}
modified src/Options.svelte
@@ -51,6 +51,14 @@
  .description :global(strong) {
    font-weight: var(--font-weight-medium);
  }
+
  .desktop {
+
    display: block !important;
+
  }
+
  @media (max-width: 720px) {
+
    .desktop {
+
      display: none !important;
+
    }
+
  }
</style>

<main>
@@ -63,7 +71,7 @@
        {option.label}
      </label>
      {#if option.description}
-
        <div class="description">
+
        <div class="description desktop">
          {@html marked(option.description.join("\n"))}
        </div>
      {/if}
modified src/Search.svelte
@@ -1,9 +1,15 @@
<script lang="ts">
  import { navigate } from 'svelte-routing';
+
  import { createEventDispatcher } from 'svelte';
+

+
  export let size = 40;

  let input = "";
+

+
  const dispatch = createEventDispatcher();
  const handleKeydown = (event: KeyboardEvent) => {
    if (event.key === 'Enter') {
+
      dispatch("search");
      navigate(`/resolver/query?${
        new URLSearchParams({ q: input })
      }`);
@@ -24,7 +30,7 @@
</style>

<input
-
    size="40"
+
    size="{size}"
    type="text"
    bind:value={input}
    on:keydown={handleKeydown}
modified src/base/home/Index.svelte
@@ -53,6 +53,15 @@
    margin-top: 1rem;
    text-align: center;
  }
+
  @media (max-width: 720px) {
+
    .blurb {
+
      max-width: none;
+
      font-size: 1rem;
+
    }
+
    .heading {
+
      font-size: 1rem;
+
    }
+
  }
</style>

<svelte:head>
modified src/base/orgs/View.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import type { SvelteComponent } from 'svelte';
  import type { Config } from '@app/config';
-
  import { formatName, explorerLink } from '@app/utils';
+
  import { formatName, explorerLink, formatAddress } from '@app/utils';
  import { session } from '@app/session';
  import Loading from '@app/Loading.svelte';
  import Modal from '@app/Modal.svelte';
@@ -16,6 +16,7 @@
  import TransferOwnership from '@app/base/orgs/TransferOwnership.svelte';
  import { Profile, ProfileType } from '@app/profile';
  import Projects from './View/Projects.svelte';
+
  import Link from '@app/Link.svelte';

  export let addressOrName: string;
  export let config: Config;
@@ -124,6 +125,12 @@
    align-items: center;
    justify-content: left;
  }
+
  .overflow-text {
+
    width: 100%;
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
    white-space: nowrap;
+
  }
  .url {
    display: flex; /* Ensures correct vertical positioning of icons */
    margin-right: 1rem;
@@ -140,6 +147,7 @@
    margin-top: 2rem;
    align-items: center;
    display: flex;
+
    flex-wrap: wrap;
  }
  .members .member {
    display: flex;
@@ -149,11 +157,35 @@
  .members .member:last-child {
    margin-right: 0;
  }
+
  .mobile {
+
    display: none !important;
+
  }
+
  .desktop {
+
    display: block !important;
+
  }
  .members .member-icon {
    width: 2rem;
    height: 2rem;
    margin-right: 1rem;
  }
+
  @media (max-width: 720px) {
+
    main {
+
      width: 100%;
+
      padding: 1.5rem;
+
    }
+
    .fields {
+
      grid-template-columns: 5rem auto;
+
    }
+
    .members .member {
+
      margin-right: 1rem;
+
    }
+
    .mobile {
+
      display: block !important;
+
    }
+
    .desktop {
+
      display: none !important;
+
    }
+
  }
</style>

<svelte:head>
@@ -178,9 +210,12 @@
          </div>
          <div class="info">
            <span class="title">
-
              <span class="bold">
+
              <span class="bold desktop">
                {profile.name ? formatName(profile.name, config) : profile.address}
              </span>
+
              <span class="bold mobile">
+
                {profile.name ? formatName(profile.name, config) : formatAddress(profile.address)}
+
              </span>
              {#if profile.name && profile.address === org.owner}
                <span class="badge">org</span>
              {/if}
@@ -188,7 +223,12 @@
            <div class="links">
              {#if profile.url}
                <a class="url" href={profile.url}>
-
                  {profile.url}
+
                  <span class="mobile">
+
                    <Icon name="url" inline />
+
                  </span>
+
                  <span class="desktop">
+
                    {profile.url}
+
                  </span>
                </a>
              {/if}
              {#if profile.twitter}
@@ -208,12 +248,14 @@
        <div class="fields">
          <!-- Address -->
          <div class="label">Address</div>
-
          <div><Address {config} address={org.address} /></div>
-
          <div></div>
+
          <div class="desktop"><Address {config} address={org.address} /></div>
+
          <div class="mobile"><Address compact {config} address={org.address} /></div>
+
          <div class="desktop" />
          <!-- Owner -->
          <div class="label">Owner</div>
-
          <div><Address resolve {config} address={org.owner} /></div>
-
          <div>
+
          <div class="desktop"><Address resolve {config} address={org.owner} /></div>
+
          <div class="mobile"><Address compact resolve {config} address={org.owner} /></div>
+
          <div class="desktop">
            {#if isOwner(org) || (account && org.isMember(account, config))}
              <button class="tiny secondary" on:click={transferOwnership}>
                Transfer
@@ -228,18 +270,27 @@
                  {` ${utils.formatBalance(token.balance)} ${token.symbol} `}
                {/each}
              </div>
-
              <div></div>
+
              <div class="desktop" />
            {/if}
          {/await}
          <!-- Seed Address -->
          {#if profile.seedId && profile.seedHost}
            <div class="label">Seed</div>
-
            <div class="seed-address">
+
            <div class="mobile">
+
              <button class="tiny faded" disabled={seedCopied} on:click={copySeed(profile.seedId, profile.seedHost)}>
+
                {#if seedCopied}
+
                  Copy ✓
+
                {:else}
+
                  Copy
+
                {/if}
+
              </button>
+
            </div>
+
            <div class="seed-address desktop">
              <span class="seed-icon">🌱</span>{
                utils.formatSeedId(profile.seedId)}@{profile.seedHost
              }<span class="faded">:{config.seed.link.port}</span>
            </div>
-
            <div>
+
            <div class="desktop">
              <button class="tiny faded" disabled={seedCopied} on:click={copySeed(profile.seedId, profile.seedHost)}>
                {#if seedCopied}
                  Copy ✓
@@ -253,14 +304,14 @@
          <div class="label">Profile</div>
          <!-- Only show the name if we aren't already using the name of the owner -->
          {#if utils.isAddressEqual(profile.address, org.address)}
-
            <div>
+
            <div class="overflow-text">
              {#if profile.name}
                <a href={profile.registry(config)} class="link">{profile.name}</a>
              {:else}
                <span class="subtle">Not set</span>
              {/if}
            </div>
-
            <div>
+
            <div class="desktop">
              {#await isAuthorized(org)}
                <!-- Loading -->
              {:then authorized}
@@ -275,7 +326,7 @@
            <div class="subtle">
              Using owner's profile.
            </div>
-
            <div>
+
            <div class="desktop">
              {#await isAuthorized(org) then authorized}
                {#if authorized}
                  <button class="tiny secondary" on:click={setName}>
@@ -292,7 +343,7 @@
              <div>
                {safe.threshold} <span class="faded">of</span> {safe.owners.length}
              </div>
-
              <div></div>
+
              <div class="desktop"/>
            {/if}
          {/await}
        </div>
@@ -308,10 +359,14 @@
                {#each members as profile}
                  <div class="member">
                    <div class="member-icon">
-
                      <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
+
                      <Link to="/users/{profile.address}">
+
                        <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
+
                      </Link>
+
                    </div>
+
                    <div class="desktop">
+
                      <Address address={profile.address} compact
+
                        resolve noAvatar {profile} {config} />
                    </div>
-
                    <Address address={profile.address} compact
-
                      resolve noAvatar {profile} {config} />
                  </div>
                {/each}
              {/await}
modified src/base/projects/Blob.svelte
@@ -27,6 +27,8 @@
  header .file-name {
    font-weight: normal;
    white-space: nowrap;
+
    overflow: hidden;
+
    text-overflow: ellipsis;
    margin-right: 1rem;
  }

modified src/base/projects/Browser.svelte
@@ -35,6 +35,8 @@
  let cloneDropdown = false;
  // Whether the seed dropdown is visible.
  let seedDropdown = false;
+
  // Whether the mobile file tree is visible.
+
  let mobileFileTree = false;

  const loadBlob = async (path: string): Promise<proj.Blob> => {
    if (state.status == Status.Loaded && state.path === path) {
@@ -59,6 +61,8 @@
    const blob = await loadBlob(path);
    getBlob = new Promise(resolve => resolve(blob));

+
    // Close mobile tree if user navigates to other file
+
    mobileFileTree = false;
    navigateBrowser(commit, path);
  };

@@ -79,6 +83,10 @@
    return proj.getTree(urn, commit, path, config);
  };

+
  const toggleMobileFileTree = () => {
+
    mobileFileTree = !mobileFileTree;
+
  };
+

  // This is reactive to respond to path changes that don't originate from this
  // component, eg. when using the browser's "back" button.
  $: getBlob = loadBlob(path);
@@ -252,8 +260,8 @@

  .source-tree {
    overflow-x: hidden;
+
    padding-top: 1rem;
  }
-

  .file-not-found {
    text-align: center;
    border-radius: 0.25rem;
@@ -267,11 +275,54 @@
    font-size: 1.5rem;
    margin-bottom: 1rem;
  }
+
  .desktop {
+
    display: default;
+
  }
+
  .mobile {
+
    display: none !important;
+
  }
+
  nav {
+
    padding: 0 2rem;
+
  }

  @media (max-width: 800px) {
    main > header, .container {
      padding-left: 2rem;
    }
+
    main > header {
+
      margin-bottom: 1.5rem;
+
    }
+
  }
+

+
  @media (max-width: 720px) {
+
    button.browse {
+
      width: 100%;
+
      border-color: var(--color-secondary-faded);
+
    }
+
    .mobile {
+
      display: block !important;
+
    }
+
    .desktop {
+
      display: none !important;
+
    }
+
    .column-right {
+
      padding: 1rem 0;
+
      min-width: 0;
+
    }
+
    .dropdown {
+
      left: 32px;
+
      z-index: 10;
+
    }
+
    .container {
+
      flex-direction: column;
+
    }
+
    .column-left {
+
      display: none;
+
      padding-right: 0;
+
    }
+
    .column-left-visible {
+
      display: block;
+
    }
  }
</style>

@@ -286,12 +337,15 @@
            {project.meta.defaultBranch}
          </div>
          <div class="hash">
-
            {commit.slice(0, 7)}
+
            {utils.formatCommit(commit)}
          </div>
        {:else}
-
          <div class="hash">
+
          <div class="hash desktop">
            {commit}
          </div>
+
          <div class="hash mobile">
+
            {utils.formatCommit(commit)}
+
          </div>
        {/if}
      </div>
      <div class="anchor">
@@ -356,9 +410,17 @@
        <strong>{tree.stats.contributors}</strong> contributor(s)
      </div>
    </header>
+

+
    <!-- Mobile navigation -->
+
    <nav class="mobile">
+
      <button class="small browse secondary center-content" on:click={toggleMobileFileTree}>
+
        Browse
+
      </button>
+
    </nav>
+

    <div class="container center-content">
      {#if tree.entries.length}
-
        <div class="column-left">
+
        <div class="column-left" class:column-left-visible={mobileFileTree}>
          <div class="source-tree">
            <Tree {tree} {path} {fetchTree} {loadingPath} on:select={onSelect} />
          </div>
modified src/base/projects/Tree/File.svelte
@@ -40,6 +40,8 @@
    margin-left: 0.25rem;
    user-select: none;
    white-space: nowrap;
+
    text-overflow: ellipsis !important;
+
    overflow: hidden;
  }
</style>

modified src/base/projects/View.svelte
@@ -71,13 +71,6 @@
  main > header {
    padding: 0 2rem 0 8rem;
  }
-

-
  @media (max-width: 800px) {
-
    main > header {
-
      padding-left: 2rem;
-
    }
-
  }
-

  .title {
    display: inline-flex;
    align-items: center;
@@ -99,10 +92,31 @@
    font-family: var(--font-family-monospace);
    font-size: 0.75rem;
    color: var(--color-foreground-faded);
+
    overflow-wrap: anywhere;
  }
  .description {
    margin: 1rem 0 1.5rem 0;
  }
+

+
  @media (max-width: 800px) {
+
    main > header {
+
      padding-left: 2rem;
+
    }
+
    main {
+
      padding-top: 2rem;
+
      min-width: 0;
+
    }
+
    .title {
+
      font-size: 1.5rem;
+
      overflow: hidden;
+
      text-overflow: ellipsis;
+
    }
+
    .org-avatar {
+
      display: inline-block;
+
      width: 1.5rem;
+
      height: 1.5rem;
+
    }
+
  }
</style>

<svelte:head>
modified src/base/projects/Widget.svelte
@@ -5,6 +5,7 @@
  import * as proj from '@app/project';
  import Loading from '@app/Loading.svelte';
  import Blockies from '@app/Blockies.svelte';
+
  import { formatCommit, formatRadicleUrn } from '@app/utils';

  enum Status { Loading, Loaded, Error }

@@ -112,35 +113,54 @@
    height: 1.25rem;
    font-size: 0.5rem;
  }
+
  .mobile {
+
    display: none !important;
+
  }
+
  .desktop {
+
    display: block !important;
+
  }
+
  @media (max-width: 720px) {
+
    article {
+
      min-width: 0;
+
    }
+
    .mobile {
+
      display: block !important;
+
    }
+
    .desktop {
+
      display: none !important;
+
    }
+
  }
</style>

<article on:click={onClick} class:has-info={info} class:project-faded={faded}>
  {#if info}
    <div class="id">
-
      <span class="name">{info.meta.name}</span><span class="urn">{project.id}</span>
+
      <span class="name">{info.meta.name}</span>
+
      <span class="urn desktop">{project.id}</span>
    </div>
    <div class="description">{info.meta.description}</div>
    <div class="anchor">
-
      <span class="commit">commit {project.anchor.stateHash}</span>
-
      <span class="actions">
-
        <slot name="actions">
-
          {#each info.meta.maintainers as urn}
-
            <span class="avatar">
-
              <Blockies address={urn} />
-
            </span>
-
          {/each}
-
        </slot>
+
      <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
+
      <span class="desktop">commit {project.anchor.stateHash}</span>
+
      <span>
+
        {#each info.meta.maintainers as urn}
+
          <span class="avatar">
+
            <Blockies address={urn} />
+
          </span>
+
        {/each}
      </span>
    </div>
  {:else}
    <div class="id">
-
      <span>{project.id}</span>
+
      <span class="desktop">{project.id}</span>
+
      <span class="mobile">{formatRadicleUrn(project.id)}</span>
      {#if state.status == Status.Loading}
        <Loading small />
      {/if}
    </div>
    <div class="anchor">
-
      <span class="commit">commit {project.anchor.stateHash}</span>
+
      <span class="commit mobile">commit {formatCommit(project.anchor.stateHash)}</span>
+
      <span class="commit desktop">commit {project.anchor.stateHash}</span>
      <span class="actions">
        <slot name="actions">
        </slot>
modified src/base/registrations/View.svelte
@@ -140,6 +140,13 @@
  main > header > * {
    margin: 0 1rem 0 0;
  }
+
  @media (max-width: 720px) {
+
    main {
+
      width: 100%;
+
      padding-left: 1rem;
+
      padding-right: 1rem;
+
    }
+
  }
</style>

<svelte:head>
@@ -172,6 +179,7 @@
    <header>
      <h1 class="bold">{subdomain}.{config.registrar.domain}</h1>
      <button
+
        style="min-width: 60px;"
        class="tiny primary" class:active={editable} disabled={!isOwner(state.owner)}
        on:click={() => editable = !editable}>
          Edit
modified src/base/users/View.svelte
@@ -101,6 +101,31 @@
    height: 2rem;
    margin-right: 1rem;
  }
+
  .mobile {
+
    display: none !important;
+
  }
+
  .desktop {
+
    display: block !important;
+
  }
+
  @media (max-width: 720px) {
+
    .fields {
+
      grid-template-columns: 5rem auto;
+
    }
+
    main {
+
      width: 100%;
+
      padding-right: 1.5rem;
+
      padding-left: 1.5rem;
+
    }
+
    .members .member {
+
      margin-right: 1rem;
+
    }
+
    .mobile {
+
      display: block !important;
+
    }
+
    .desktop {
+
      display: none !important;
+
    }
+
  }
</style>

<svelte:head>
@@ -121,7 +146,14 @@
        </span>
        <div class="links">
          {#if profile.url}
-
            <a class="url" href={profile.url}>{profile.url}</a>
+
            <a class="url" href={profile.url}>
+
              <div class="mobile">
+
                <Icon name="url" inline />
+
              </div>
+
              <div class="desktop">
+
                {profile.url}
+
              </div>
+
            </a>
          {/if}
          {#if profile.twitter}
            <a class="url" href="https://twitter.com/{profile.twitter}">
@@ -139,13 +171,15 @@
      <div class="fields">
        <!-- Address -->
        <div class="label">Address</div>
-
        <div><Address {config} address={profile.address} /></div>
-
        <div></div>
+
        <div class="desktop"><Address {config} address={profile.address} /></div>
+
        <div class="mobile"><Address compact {config} address={profile.address} /></div>
+
        <div class="desktop" />
        <!-- Project anchors -->
        {#if profile.anchorsAccount}
          <div class="label">Anchors</div>
-
          <div><Address {config} address={profile.anchorsAccount} /></div>
-
          <div></div>
+
          <div class="desktop"><Address {config} address={profile.anchorsAccount} /></div>
+
          <div class="mobile"><Address compact {config} address={profile.anchorsAccount} /></div>
+
          <div class="desktop" />
        {/if}
        <!-- Profile -->
        <div class="label">Profile</div>
@@ -156,7 +190,7 @@
            <span class="subtle">Not set</span>
          {/if}
        </div>
-
        <div>
+
        <div class="desktop">
          {#if (isAuthorized(profile.address))}
            <button class="tiny secondary" on:click={setName}>
              Set
@@ -177,8 +211,14 @@
                  <div class="member-icon">
                    <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
                  </div>
-
                  <Address address={profile.address} compact
-
                    resolve noBadge noAvatar {profile} {config} />
+
                  <div class="desktop">
+
                    <Address address={profile.address}
+
                      resolve noBadge noAvatar {profile} {config} />
+
                  </div>
+
                  <div class="mobile">
+
                    <Address address={profile.address} compact
+
                      resolve noBadge noAvatar {profile} {config} />
+
                  </div>
                </div>
              {/await}
            {/each}
modified src/utils.ts
@@ -82,6 +82,12 @@ export function formatSeedId(id: string): string {
    + id.substring(id.length - 6, id.length);
}

+
export function formatRadicleUrn(id: string): string {
+
  return id.substring(0, 14)
+
    + '…'
+
    + id.substring(id.length - 6, id.length);
+
}
+

export function formatBalance(n: BigNumber): string {
  return ethers.utils.commify(parseFloat(ethers.utils.formatUnits(n)).toFixed(2));
}
@@ -110,6 +116,10 @@ export function formatHash(hash: string): string {
    + hash.substring(hash.length - 4, hash.length);
}

+
export function formatCommit(oid: string): string {
+
  return oid.substring(0,7);
+
}
+

export function formatOrg(input: string, config: Config): string {
  if (isAddress(input)) {
    return ethers.utils.getAddress(input);
@@ -132,6 +142,22 @@ export function parseEnsLabel(name: string, config: Config): string {
  return label;
}

+
export function clickOutside(node: HTMLElement, onEventFunction: () => void): any {
+
  const handleClick = (event: any) => {
+
    const path = event.composedPath();
+
    if (!path.includes(node)) {
+
      onEventFunction();
+
    }
+
  };
+
  document.addEventListener("click", handleClick, true);
+

+
  return {
+
    destroy() {
+
      document.removeEventListener("click", handleClick, true);
+
    }
+
  };
+
}
+

// Takes a URL, eg. "https://twitter.com/cloudhead", and return "cloudhead".
// Returns the original string if it was unable to extract the username.
export function parseUsername(input: string): string {