mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-31 01:00:47 +08:00
Organise files more consistently
Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<script lang="ts">
|
||||
import type { Wallet } from "@app/lib/wallet";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import Avatar from "@app/components/Avatar.svelte";
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import {
|
||||
AddressType,
|
||||
explorerLink,
|
||||
formatAddress,
|
||||
identifyAddress,
|
||||
parseEnsLabel,
|
||||
} from "@app/lib/utils";
|
||||
import { Profile, ProfileType } from "@app/lib/profile";
|
||||
|
||||
export let address: string;
|
||||
export let wallet: Wallet;
|
||||
export let resolve = false;
|
||||
export let noBadge = false;
|
||||
export let noAvatar = false;
|
||||
export let compact = false;
|
||||
export let small = false;
|
||||
export let tiny = false;
|
||||
export let highlight = false;
|
||||
// This property allows components eg. Header.svelte to pass a resolved profile object.
|
||||
export let profile: Profile | null = null;
|
||||
|
||||
let addressType: AddressType | null = null;
|
||||
|
||||
const addressOrName = profile?.ens?.name || address;
|
||||
|
||||
onMount(async () => {
|
||||
if (!profile) {
|
||||
identifyAddress(address, wallet).then(
|
||||
(t: AddressType) => (addressType = t),
|
||||
);
|
||||
|
||||
if (resolve) {
|
||||
Profile.get(address, ProfileType.Minimal, wallet).then(
|
||||
p => (profile = p),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If there is a profile we can use the profile.type to avoid identifying it again.
|
||||
addressType = profile.type;
|
||||
}
|
||||
});
|
||||
$: addressLabel =
|
||||
resolve && profile?.name
|
||||
? compact
|
||||
? parseEnsLabel(profile.name, wallet)
|
||||
: profile.name
|
||||
: checksumAddress;
|
||||
$: checksumAddress = compact
|
||||
? formatAddress(address)
|
||||
: ethers.utils.getAddress(address);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.address {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.address a {
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
.address a:hover {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
.highlight {
|
||||
color: var(--color-foreground-6);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.wrapper {
|
||||
gap: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
class="address"
|
||||
title={address}
|
||||
class:txt-small={small}
|
||||
class:txt-tiny={tiny}
|
||||
class:highlight>
|
||||
{#if !noAvatar}
|
||||
{#if resolve && profile?.avatar}
|
||||
<Avatar inline source={profile.avatar} title={address} />
|
||||
{:else}
|
||||
<Avatar inline source={address} title={address} />
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="wrapper">
|
||||
{#if addressType === AddressType.Org}
|
||||
<Link
|
||||
route={{
|
||||
resource: "profile",
|
||||
params: { addressOrName: addressOrName },
|
||||
}}>
|
||||
{addressLabel}
|
||||
</Link>
|
||||
{#if !noBadge}
|
||||
<Badge variant="foreground">org</Badge>
|
||||
{/if}
|
||||
{:else if addressType === AddressType.Contract}
|
||||
<Link route={{ resource: "profile", params: { addressOrName: address } }}>
|
||||
{addressLabel}
|
||||
</Link>
|
||||
{#if !noBadge}
|
||||
<Badge variant="foreground">contract</Badge>
|
||||
{/if}
|
||||
{:else if addressType === AddressType.EOA}
|
||||
<Link
|
||||
route={{
|
||||
resource: "profile",
|
||||
params: { addressOrName: addressOrName },
|
||||
}}>
|
||||
{addressLabel}
|
||||
</Link>
|
||||
{:else}
|
||||
<!-- While we're waiting to find out what address type it is -->
|
||||
<a href={explorerLink(address, wallet)} target="_blank" rel="noreferrer">
|
||||
{addressLabel}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
|
||||
type T = $$Generic;
|
||||
|
||||
export let fetch: Promise<T>;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.error {
|
||||
color: var(--color-negative);
|
||||
background-color: var(--color-negative-2);
|
||||
word-wrap: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
padding: 1rem;
|
||||
}
|
||||
.error::selection,
|
||||
.error ::selection {
|
||||
background-color: var(--color-negative);
|
||||
}
|
||||
</style>
|
||||
|
||||
{#await fetch}
|
||||
<Loading center />
|
||||
{:then result}
|
||||
<slot {result} />
|
||||
{:catch err}
|
||||
<div class="error txt-tiny">
|
||||
<div>
|
||||
API request to <span class="txt-monospace">{err.url}</span>
|
||||
failed.
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import type { Author } from "@app/lib/cobs";
|
||||
import type { Wallet } from "@app/lib/wallet";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Address from "@app/components/Address.svelte";
|
||||
import { Profile, ProfileType } from "@app/lib/profile";
|
||||
import { formatRadicleUrn, formatTimestamp } from "@app/lib/utils";
|
||||
|
||||
export let noAvatar = false;
|
||||
export let author: Author;
|
||||
export let timestamp: number;
|
||||
export let caption: string;
|
||||
export let wallet: Wallet;
|
||||
export let profile: Profile | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
if (author.profile?.ens?.name) {
|
||||
profile = await Profile.get(
|
||||
author.profile.ens.name,
|
||||
ProfileType.Minimal,
|
||||
wallet,
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.authorship {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-foreground);
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
.caption {
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.highlight {
|
||||
color: var(--color-foreground-6);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.date {
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="authorship txt-tiny">
|
||||
{#if profile}
|
||||
<Address
|
||||
tiny
|
||||
highlight
|
||||
resolve
|
||||
noBadge
|
||||
compact
|
||||
{noAvatar}
|
||||
{wallet}
|
||||
{profile}
|
||||
address={profile.address} />
|
||||
{:else if author.profile}
|
||||
<span class="highlight">
|
||||
{author.profile.name}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="highlight">
|
||||
{formatRadicleUrn(author.urn)}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="caption"> {caption} </span>
|
||||
<span class="txt-tiny date">
|
||||
{formatTimestamp(timestamp)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { createIcon } from "@app/lib/blockies";
|
||||
import { isAddress, isPeerId, isRadicleId } from "@app/lib/utils";
|
||||
|
||||
export let title: string;
|
||||
export let source: string;
|
||||
export let inline = false;
|
||||
export let grayscale = false;
|
||||
|
||||
function handleMissingFile() {
|
||||
console.warn("Not able to locate", source);
|
||||
source = createContainer(title);
|
||||
}
|
||||
|
||||
function createContainer(source: string) {
|
||||
const seed = source.toLowerCase();
|
||||
const avatar = createIcon({
|
||||
seed,
|
||||
size: 8,
|
||||
scale: 16,
|
||||
});
|
||||
return avatar.toDataURL();
|
||||
}
|
||||
|
||||
if (isAddress(source) || isRadicleId(source) || isPeerId(source)) {
|
||||
source = createContainer(source);
|
||||
}
|
||||
grayscale = isPeerId(title) || isRadicleId(title);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.avatar {
|
||||
display: block;
|
||||
border-radius: var(--border-radius-round);
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
height: 100%;
|
||||
width: inherit;
|
||||
object-fit: cover;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.grayscale {
|
||||
filter: grayscale();
|
||||
}
|
||||
.inline {
|
||||
display: inline-block !important;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<img
|
||||
{title}
|
||||
src={source}
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
on:error={handleMissingFile}
|
||||
class:inline
|
||||
class:grayscale />
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
export let variant:
|
||||
| "caution"
|
||||
| "foreground"
|
||||
| "negative"
|
||||
| "positive"
|
||||
| "primary"
|
||||
| "tertiary";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: var(--font-size-tiny);
|
||||
line-height: 1.6;
|
||||
height: var(--button-tiny-height);
|
||||
display: flex;
|
||||
}
|
||||
.foreground {
|
||||
color: var(--color-foreground-6);
|
||||
background: var(--color-foreground-2);
|
||||
}
|
||||
.positive {
|
||||
color: var(--color-positive-6);
|
||||
background-color: var(--color-positive-3);
|
||||
}
|
||||
.negative {
|
||||
color: var(--color-negative);
|
||||
background-color: var(--color-negative-4);
|
||||
}
|
||||
.primary {
|
||||
color: var(--color-primary);
|
||||
background: linear-gradient(var(--color-primary-3), var(--color-primary-3)),
|
||||
linear-gradient(var(--color-background), var(--color-background));
|
||||
}
|
||||
.tertiary {
|
||||
color: var(--color-tertiary-6);
|
||||
background: var(--color-tertiary-1);
|
||||
}
|
||||
.caution {
|
||||
color: var(--color-caution);
|
||||
background: var(--color-caution-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<span
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
class="badge"
|
||||
class:caution={variant === "caution"}
|
||||
class:foreground={variant === "foreground"}
|
||||
class:negative={variant === "negative"}
|
||||
class:positive={variant === "positive"}
|
||||
class:primary={variant === "primary"}
|
||||
class:tertiary={variant === "tertiary"}>
|
||||
<slot />
|
||||
</span>
|
||||
@@ -0,0 +1,134 @@
|
||||
<script lang="ts">
|
||||
export let title: string | undefined = undefined;
|
||||
export let variant:
|
||||
| "foreground"
|
||||
| "negative"
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "text";
|
||||
export let size: "tiny" | "small" | "regular" = "regular";
|
||||
|
||||
export let disabled: boolean = false;
|
||||
export let waiting: boolean = false;
|
||||
export let style: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
button {
|
||||
background: transparent;
|
||||
border-radius: var(--border-radius-round);
|
||||
border: 1px solid var(--color-foreground);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-feature-settings: "ss01", "ss02", "cv01", "cv03";
|
||||
font-size: var(--font-size-regular);
|
||||
line-height: 1.6rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
button[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
button:not([disabled]):hover {
|
||||
color: var(--color-background);
|
||||
}
|
||||
.foreground {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
.foreground[disabled] {
|
||||
color: var(--color-foreground-5);
|
||||
border-color: var(--color-foreground-5);
|
||||
}
|
||||
.foreground:not([disabled]):hover {
|
||||
background-color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.primary[disabled] {
|
||||
color: var(--color-primary-5);
|
||||
border-color: var(--color-primary-5);
|
||||
}
|
||||
.primary:not([disabled]):hover {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--color-secondary);
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
.secondary[disabled] {
|
||||
color: var(--color-secondary-5);
|
||||
border-color: var(--color-secondary-5);
|
||||
}
|
||||
.secondary:not([disabled]):hover {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: var(--color-negative);
|
||||
border-color: var(--color-negative);
|
||||
}
|
||||
.negative[disabled] {
|
||||
color: var(--color-negative-5);
|
||||
border-color: var(--color-negative-5);
|
||||
}
|
||||
.negative:not([disabled]):hover {
|
||||
background-color: var(--color-negative);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--color-foreground);
|
||||
border: none;
|
||||
}
|
||||
.text[disabled] {
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.text:not([disabled]):hover {
|
||||
background-color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.tiny {
|
||||
font-size: var(--font-size-tiny);
|
||||
height: var(--button-small-tiny);
|
||||
padding: 0 0.6rem;
|
||||
}
|
||||
.small {
|
||||
font-size: var(--font-size-small);
|
||||
height: var(--button-small-height);
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
.regular {
|
||||
height: var(--button-regular-height);
|
||||
padding: 0 1.5rem;
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.waiting {
|
||||
cursor: waiting;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button
|
||||
{title}
|
||||
{disabled}
|
||||
{style}
|
||||
on:click|stopPropagation
|
||||
on:focus
|
||||
on:blur
|
||||
on:mouseout
|
||||
on:mouseover
|
||||
class:foreground={variant === "foreground"}
|
||||
class:negative={variant === "negative"}
|
||||
class:primary={variant === "primary"}
|
||||
class:secondary={variant === "secondary"}
|
||||
class:text={variant === "text"}
|
||||
class:tiny={size === "tiny"}
|
||||
class:small={size === "small"}
|
||||
class:regular={size === "regular"}
|
||||
class:waiting>
|
||||
<slot />
|
||||
</button>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import { toClipboard } from "@app/lib/utils";
|
||||
|
||||
const dispatch = createEventDispatcher<{ copied: never }>();
|
||||
|
||||
const copy = () => {
|
||||
toClipboard(text);
|
||||
dispatch("copied");
|
||||
};
|
||||
|
||||
export let text: string;
|
||||
export let small = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.clipboard {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.clipboard.small {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
.clipboard:hover :global(svg) {
|
||||
fill: var(--color-foreground);
|
||||
}
|
||||
.clipboard:active :global(svg) {
|
||||
fill: var(--color-foreground-6);
|
||||
}
|
||||
.clipboard:hover {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span class="clipboard" class:small on:click|stopPropagation={copy}>
|
||||
{#if small}
|
||||
<Icon name="clipboard-small" />
|
||||
{:else}
|
||||
<Icon name="clipboard" />
|
||||
{/if}
|
||||
</span>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import type { Blob } from "@app/lib/project";
|
||||
import type { Comment, Thread } from "@app/lib/issue";
|
||||
import type { Wallet } from "@app/lib/wallet";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
import Avatar from "@app/components/Avatar.svelte";
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
import ReactionSelector from "./Comment/ReactionSelector.svelte";
|
||||
import Reactions from "./Comment/Reactions.svelte";
|
||||
|
||||
import { Profile, ProfileType } from "@app/lib/profile";
|
||||
|
||||
export let comment: Comment | Thread;
|
||||
export let wallet: Wallet;
|
||||
export let caption = "left a comment";
|
||||
export let getImage: (path: string) => Promise<Blob>;
|
||||
|
||||
let profile: Profile | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
if (comment.author.profile?.ens?.name) {
|
||||
profile = await Profile.get(
|
||||
comment.author.profile.ens.name,
|
||||
ProfileType.Minimal,
|
||||
wallet,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const templateComment = `<!--
|
||||
Please enter a comment message for your patch update. Leaving this
|
||||
blank is also okay.
|
||||
-->`;
|
||||
|
||||
$: source = profile?.avatar || comment.author.urn;
|
||||
$: title =
|
||||
profile?.name ||
|
||||
(comment.author.profile ? comment.author.profile.name : comment.author.urn);
|
||||
|
||||
const selectReaction = (event: { detail: string }) => {
|
||||
// TODO: Once we allow adding reactions through the http-api, we should call it here.
|
||||
console.debug(event.detail);
|
||||
};
|
||||
|
||||
const incrementReaction = (event: { detail: string }) => {
|
||||
// TODO: Once we allow increment reactions through the http-api, we should call it here.
|
||||
console.debug(event.detail);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.comment {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
}
|
||||
.person {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.card {
|
||||
flex: 1;
|
||||
border: 1px solid var(--color-foreground-4);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.card-body {
|
||||
font-size: var(--font-size-small);
|
||||
padding: 0rem 1rem 1rem 1rem;
|
||||
}
|
||||
.reactions {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="comment">
|
||||
<div class="person">
|
||||
<Avatar {source} {title} />
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<Authorship
|
||||
noAvatar
|
||||
{wallet}
|
||||
{caption}
|
||||
{profile}
|
||||
author={comment.author}
|
||||
timestamp={comment.timestamp} />
|
||||
<ReactionSelector on:select={selectReaction} />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{#if comment.body.trim() === "" || comment.body.trim() === templateComment}
|
||||
<span class="txt-missing">No description.</span>
|
||||
{:else}
|
||||
<Markdown content={comment.body} {getImage} />
|
||||
{/if}
|
||||
{#if comment.reactions.length > 0}
|
||||
<div class="reactions">
|
||||
<Reactions
|
||||
reactions={comment.reactions}
|
||||
on:click={incrementReaction} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
<!-- TODO: Once we are able to add reactions, we should allow people to interact with the reaction handler -->
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import { config } from "@app/lib/config";
|
||||
|
||||
const showReactions = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: string }>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
color: var(--color-foreground-5);
|
||||
border-radius: var(--border-radius);
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.selector > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
left: 1.5rem;
|
||||
background-color: var(--color-foreground-1);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.modal > div {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.modal > div:last-child {
|
||||
border-top-right-radius: var(--border-radius-small);
|
||||
border-bottom-right-radius: var(--border-radius-small);
|
||||
}
|
||||
.modal > div:first-child {
|
||||
border-top-left-radius: var(--border-radius-small);
|
||||
border-bottom-left-radius: var(--border-radius-small);
|
||||
}
|
||||
.modal > div:hover {
|
||||
background-color: var(--color-foreground-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="selector">
|
||||
<Icon name="ellipsis" />
|
||||
{#if showReactions}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="modal">
|
||||
{#each config.reactions as reaction}
|
||||
<div on:click={() => dispatch("select", reaction)}>
|
||||
{reaction}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
|
||||
export let reactions: Record<string, number> | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher<{ click: string }>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.reactions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if reactions}
|
||||
<div class="reactions">
|
||||
{#each Object.entries(reactions) as [reaction, count]}
|
||||
<!-- TODO: Remove the disabled attribute once we are able to increment reactions -->
|
||||
<Button
|
||||
variant="foreground"
|
||||
size="tiny"
|
||||
on:click={() => dispatch("click", reaction)}>
|
||||
{reaction}
|
||||
{count}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import type { Err } from "@app/lib/error";
|
||||
import type { Wallet } from "@app/lib/wallet";
|
||||
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import ConnectWallet from "@app/components/Connect/ConnectWallet.svelte";
|
||||
import ErrorModal from "@app/components/ErrorModal.svelte";
|
||||
|
||||
import { Connection, state } from "@app/lib/session";
|
||||
|
||||
export let caption = "Connect";
|
||||
export let wallet: Wallet;
|
||||
export let buttonVariant: "foreground" | "primary";
|
||||
|
||||
let error: Err | null = null;
|
||||
|
||||
const onModalClose = () => {
|
||||
const wcs = get(wallet.walletConnect.state);
|
||||
|
||||
if (wcs.state === "open") {
|
||||
wallet.walletConnect.state.set({ state: "close" });
|
||||
wcs.onClose();
|
||||
}
|
||||
};
|
||||
const onConnect = async () => {
|
||||
try {
|
||||
await state.connectWalletConnect(wallet);
|
||||
} catch (e: any) {
|
||||
walletConnectState.set({ state: "close" });
|
||||
error = e;
|
||||
}
|
||||
};
|
||||
|
||||
$: connecting = $state.connection === Connection.Connecting;
|
||||
$: walletConnectState = wallet.walletConnect.state;
|
||||
</script>
|
||||
|
||||
<Button
|
||||
on:click={onConnect}
|
||||
variant={buttonVariant}
|
||||
disabled={connecting}
|
||||
waiting={connecting}>
|
||||
{#if connecting}
|
||||
Connecting…
|
||||
{:else}
|
||||
{caption}
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
{#if $walletConnectState.state === "open"}
|
||||
<ConnectWallet
|
||||
{wallet}
|
||||
uri={$walletConnectState.uri}
|
||||
on:close={onModalClose} />
|
||||
{:else if error}
|
||||
<ErrorModal
|
||||
floating
|
||||
emoji="👛"
|
||||
title="Connection failed"
|
||||
{error}
|
||||
on:close={() => (error = null)} />
|
||||
{/if}
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Wallet } from "@app/wallet";
|
||||
import type { Wallet } from "@app/lib/wallet";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { qrcode } from "pure-svg-code";
|
||||
|
||||
import Button from "@app/Button.svelte";
|
||||
import Modal from "@app/Modal.svelte";
|
||||
import { state } from "@app/session";
|
||||
import { twemoji } from "@app/utils";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Modal from "@app/components/Modal.svelte";
|
||||
import { state } from "@app/lib/session";
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
export let uri: string;
|
||||
export let wallet: Wallet;
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
|
||||
export let items: {
|
||||
key: string;
|
||||
title: string;
|
||||
value: string;
|
||||
badge: string | null;
|
||||
}[];
|
||||
export let selected: string | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: string }>();
|
||||
const onSelect = (item: string) => {
|
||||
dispatch("select", item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dropdown {
|
||||
background-color: var(--color-foreground-1);
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
position: absolute;
|
||||
box-shadow: var(--elevation-low);
|
||||
z-index: 10;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.dropdown-item:hover,
|
||||
.selected {
|
||||
background-color: var(--color-foreground-2);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.dropdown {
|
||||
left: 32px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="dropdown">
|
||||
{#each items as { key, value, badge, title }}
|
||||
{#if key && value}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="dropdown-item"
|
||||
class:selected={value === selected}
|
||||
use:twemoji
|
||||
on:click={() => onSelect(value)}
|
||||
{title}>
|
||||
{@html key}
|
||||
{#if badge}
|
||||
<Badge variant="primary">{badge}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Err } from "@app/lib/error";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Modal from "@app/components/Modal.svelte";
|
||||
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
const dispatch = createEventDispatcher<{ close: never }>();
|
||||
|
||||
export let error: Err | null = null;
|
||||
export let title = "Error";
|
||||
export let emoji = "";
|
||||
export let subtitle = "";
|
||||
export let message = "";
|
||||
export let floating = false;
|
||||
export let subtle = false;
|
||||
export let action = floating ? "Close" : "Back";
|
||||
|
||||
const body = message || (error && error.message) || "";
|
||||
</script>
|
||||
|
||||
<Modal on:close error {floating} {subtle}>
|
||||
<span slot="title" use:twemoji>
|
||||
{#if emoji}
|
||||
<div>{emoji}</div>
|
||||
{/if}
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<span slot="subtitle">
|
||||
{subtitle}
|
||||
</span>
|
||||
|
||||
<span slot="body">
|
||||
<slot>
|
||||
<span class="txt-bold">Error:</span>
|
||||
{body}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span slot="actions">
|
||||
<slot name="actions">
|
||||
<Button variant="negative" on:click={() => dispatch("close")}>
|
||||
{action}
|
||||
</Button>
|
||||
</slot>
|
||||
</span>
|
||||
</Modal>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" context="module">
|
||||
import { writable } from "svelte/store";
|
||||
const focused = writable<HTMLDivElement | undefined>(undefined);
|
||||
|
||||
export function closeFocused() {
|
||||
focused.set(undefined);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let disabled = false;
|
||||
export let overlay = false;
|
||||
|
||||
let expanded = false;
|
||||
let thisComponent: HTMLDivElement;
|
||||
|
||||
function clickOutside(ev: MouseEvent) {
|
||||
if (!$focused?.contains(ev.target as HTMLDivElement)) {
|
||||
closeFocused();
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (!disabled) {
|
||||
expanded = !expanded;
|
||||
if ($focused === thisComponent) {
|
||||
closeFocused();
|
||||
} else {
|
||||
focused.set(thisComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: expanded = $focused === thisComponent;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.overlay {
|
||||
background-color: #00000075;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:window on:click={clickOutside} />
|
||||
|
||||
<div bind:this={thisComponent}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:click={toggle}
|
||||
class="toggle"
|
||||
style:cursor={disabled ? "not-allowed" : "pointer"}>
|
||||
<slot name="toggle" />
|
||||
</div>
|
||||
|
||||
{#if expanded}
|
||||
{#if overlay}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="overlay" on:click={toggle} />
|
||||
{/if}
|
||||
<slot name="modal" />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,282 @@
|
||||
<script context="module" lang="ts">
|
||||
export interface RegistrationRecord {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
name: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
validate?: string;
|
||||
placeholder?: string;
|
||||
description: string;
|
||||
resolve?: boolean;
|
||||
editable: boolean;
|
||||
error?: string | null;
|
||||
example?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const validationExamples: Record<string, string> = {
|
||||
URL: "https://acme.xyz/",
|
||||
URN: "eip155:1:0xd1bb21bd5a432d2919c82bcefe1bc7f8cc9207d9",
|
||||
handle: "acme",
|
||||
id: "hydkkcf6k9be5fuszdhpqbctu3q3fuwagj874wx2puia8ti8coygh1",
|
||||
identity: "rad:git:hnrkqdpm9ub19oc8dccx44echy75hzfsezyio",
|
||||
domain: "seed.acme.xyz",
|
||||
address: "0x17a8c096733BD5F87aD43D7A2A4d1C42ab8A2A70",
|
||||
};
|
||||
|
||||
const validationTypes: { [index: string]: RegExp } = {
|
||||
URL: /^(https:\/\/|http:\/\/|ipfs:\/\/)\S+/,
|
||||
URN: /^[a-z]+:[a-zA-Z0-9:-]{1,64}$/,
|
||||
// Github
|
||||
// Username may only contain alphanumeric characters or hyphens.
|
||||
// Username cannot have multiple consecutive hyphens.
|
||||
// Username cannot begin or end with a hyphen.
|
||||
// Maximum is 39 characters.
|
||||
// Twitter
|
||||
// Username may only contain alphanumeric characters or underscores.
|
||||
// Maximum is 15 characters.
|
||||
// For simplification of the regex pattern we use a combined version of both requirements.
|
||||
handle: /^[a-zA-Z0-9-_]{1,39}$/,
|
||||
address: /^0x[a-zA-Z0-9]{40}$/,
|
||||
id: /^[a-z0-9]+$/,
|
||||
identity: /^rad:git:[a-z0-9]{37}$/,
|
||||
domain: /^[^/:$!_;,@#]+\.[a-z]{2,}$/,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Wallet } from "@app/lib/wallet";
|
||||
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { marked } from "marked";
|
||||
import {
|
||||
markdownExtensions as extensions,
|
||||
capitalize,
|
||||
isUrl,
|
||||
isAddress,
|
||||
formatSeedId,
|
||||
} from "@app/lib/utils";
|
||||
|
||||
import Address from "@app/components/Address.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
|
||||
export let fields: Field[];
|
||||
export let editable = false;
|
||||
export let disabled = false;
|
||||
export let wallet: Wallet;
|
||||
|
||||
let formFields = cloneDeep(fields);
|
||||
let hasErrors = false;
|
||||
|
||||
marked.use({ extensions });
|
||||
|
||||
const check = (event: Event): void => {
|
||||
const name = (<HTMLInputElement>event.target).name;
|
||||
const value = (<HTMLInputElement>event.target).value;
|
||||
|
||||
formFields = formFields.map(field => {
|
||||
if (field.name === name && field.validate) {
|
||||
hasErrors =
|
||||
value.length > 0
|
||||
? !validationTypes[field.validate].test(value)
|
||||
: false;
|
||||
return {
|
||||
...field,
|
||||
value: value,
|
||||
error: hasErrors ? `Must be a valid ${field.validate}` : undefined,
|
||||
example: validationExamples[field.validate],
|
||||
};
|
||||
}
|
||||
return field;
|
||||
});
|
||||
};
|
||||
|
||||
const cleanup = (fields: Field[]): RegistrationRecord[] => {
|
||||
return fields
|
||||
.filter(field => field.editable)
|
||||
.map(field => {
|
||||
return {
|
||||
name: field.name,
|
||||
value: field.value ? field.value.trim() : "",
|
||||
};
|
||||
});
|
||||
};
|
||||
const dispatch = createEventDispatcher<{
|
||||
save: RegistrationRecord[];
|
||||
validate: never;
|
||||
cancel: never;
|
||||
}>();
|
||||
const save = () => dispatch("save", cleanup(formFields));
|
||||
function validate(event: Event) {
|
||||
check(event);
|
||||
dispatch("validate");
|
||||
}
|
||||
const cancel = () => {
|
||||
formFields = cloneDeep(fields);
|
||||
dispatch("cancel");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem auto;
|
||||
gap: 1rem 1.5rem;
|
||||
}
|
||||
.fields > div {
|
||||
place-self: center start;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 28rem;
|
||||
height: 2.5rem;
|
||||
border: 1px dashed transparent;
|
||||
padding: 0.25rem 1rem;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ellipsis {
|
||||
width: 28rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding-left: 1rem;
|
||||
max-width: 32rem;
|
||||
}
|
||||
.description :global(p) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description.invalid {
|
||||
color: var(--color-negative);
|
||||
}
|
||||
|
||||
.label {
|
||||
border: 1px solid transparent;
|
||||
padding: 0.25rem;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
visibility: hidden;
|
||||
gap: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.actions.editable {
|
||||
visibility: visible;
|
||||
}
|
||||
.text-input {
|
||||
width: 28rem;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.field {
|
||||
width: unset;
|
||||
}
|
||||
.text-input {
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="fields">
|
||||
{#each formFields as field}
|
||||
<div class="label txt-highlight">
|
||||
{field.label || capitalize(field.name)}
|
||||
</div>
|
||||
<div>
|
||||
{#if field.editable && editable}
|
||||
<div class="text-input">
|
||||
<TextInput
|
||||
variant="dashed"
|
||||
name={field.name}
|
||||
placeholder={field.placeholder}
|
||||
on:change={validate}
|
||||
on:input={() => (field.error = null)}
|
||||
bind:value={field.value}
|
||||
{disabled} />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="field">
|
||||
{#if field.value}
|
||||
{#if isUrl(field.value)}
|
||||
<span class="ellipsis">
|
||||
<a
|
||||
href={field.value}
|
||||
class="txt-link"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
{field.value}
|
||||
</a>
|
||||
</span>
|
||||
{:else if isAddress(field.value)}
|
||||
<div class="layout-desktop-inline">
|
||||
<Address
|
||||
resolve={field.resolve ?? false}
|
||||
address={field.value}
|
||||
{wallet} />
|
||||
</div>
|
||||
<div class="layout-mobile-inline">
|
||||
<Address
|
||||
compact
|
||||
resolve={field.resolve ?? false}
|
||||
address={field.value}
|
||||
{wallet} />
|
||||
</div>
|
||||
{:else if field.url}
|
||||
<div>
|
||||
<a href={field.url} class="txt-link">{field.value}</a>
|
||||
</div>
|
||||
{:else if field.validate === "id"}
|
||||
<div class="layout-mobile">
|
||||
{formatSeedId(field.value)}
|
||||
</div>
|
||||
<div class="layout-desktop">
|
||||
{field.value}
|
||||
</div>
|
||||
{:else}
|
||||
{field.value}
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="txt-missing">✗ Not set</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{#if field.error}
|
||||
<div class="description txt-faded txt-small invalid">
|
||||
{#if field.example}
|
||||
{field.error}, eg.
|
||||
<em>{field.example}</em>
|
||||
{:else}
|
||||
{field.error}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="description txt-faded txt-small">
|
||||
{@html marked(field.description)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="actions" class:editable>
|
||||
<Button on:click={cancel} {disabled} variant="foreground">Cancel</Button>
|
||||
<Button on:click={save} disabled={hasErrors || disabled} variant="primary">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,321 @@
|
||||
<script lang="ts">
|
||||
import { unreachable } from "@app/lib/utils";
|
||||
|
||||
export let name:
|
||||
| "browse"
|
||||
| "clipboard"
|
||||
| "clipboard-small"
|
||||
| "ellipsis"
|
||||
| "fork"
|
||||
| "github"
|
||||
| "moon"
|
||||
| "checkmark"
|
||||
| "sun"
|
||||
| "twitter"
|
||||
| "gear"
|
||||
| "chevron-down"
|
||||
| "chevron-up"
|
||||
| "url";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<svg
|
||||
role="img"
|
||||
on:click
|
||||
height="24"
|
||||
width="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
{#if name === "browse"}
|
||||
<path
|
||||
d="M8.46934 7.23871C8.61151 7.10623 8.79956 7.03411 8.99386
|
||||
7.03753C9.18816 7.04096 9.37355 7.11967 9.51096 7.25709C9.64838 7.3945
|
||||
9.72709 7.57988 9.73052 7.77419C9.73394 7.96849 9.66182 8.15653 9.52934
|
||||
8.29871L5.80934 12.0187L9.52934 15.7387C9.60303 15.8074 9.66213 15.8902
|
||||
9.70312 15.9822C9.74411 16.0742 9.76615 16.1735 9.76793 16.2742C9.76971
|
||||
16.3749 9.75118 16.4749 9.71346 16.5683C9.67574 16.6617 9.6196 16.7465
|
||||
9.54838 16.8177C9.47716 16.889 9.39233 16.9451 9.29894 16.9828C9.20555
|
||||
17.0206 9.10552 17.0391 9.00482 17.0373C8.90411 17.0355 8.8048 17.0135
|
||||
8.7128 16.9725C8.6208 16.9315 8.538 16.8724 8.46934 16.7987L4.21934
|
||||
12.5487C4.07889 12.4081 4 12.2175 4 12.0187C4 11.82 4.07889 11.6293 4.21934
|
||||
11.4887L8.46934 7.23871V7.23871ZM15.0293 7.23871C14.9607 7.16502 14.8779
|
||||
7.10592 14.7859 7.06493C14.6939 7.02394 14.5946 7.00189 14.4939
|
||||
7.00012C14.3932 6.99834 14.2931 7.01686 14.1997 7.05459C14.1064 7.09231
|
||||
14.0215 7.14845 13.9503 7.21967C13.8791 7.29089 13.8229 7.37572 13.7852
|
||||
7.46911C13.7475 7.5625 13.729 7.66253 13.7307 7.76323C13.7325 7.86393
|
||||
13.7546 7.96325 13.7956 8.05525C13.8366 8.14725 13.8957 8.23005 13.9693
|
||||
8.29871L17.6893 12.0187L13.9693 15.7387C13.8957 15.8074 13.8366 15.8902
|
||||
13.7956 15.9822C13.7546 16.0742 13.7325 16.1735 13.7307 16.2742C13.729
|
||||
16.3749 13.7475 16.4749 13.7852 16.5683C13.8229 16.6617 13.8791 16.7465
|
||||
13.9503 16.8177C14.0215 16.889 14.1064 16.9451 14.1997 16.9828C14.2931
|
||||
17.0206 14.3932 17.0391 14.4939 17.0373C14.5946 17.0355 14.6939 17.0135
|
||||
14.7859 16.9725C14.8779 16.9315 14.9607 16.8724 15.0293 16.7987L19.2793
|
||||
12.5487C19.4198 12.4081 19.4987 12.2175 19.4987 12.0187C19.4987 11.82
|
||||
19.4198 11.6293 19.2793 11.4887L15.0293 7.23871V7.23871Z" />
|
||||
{:else if name === "clipboard"}
|
||||
<path
|
||||
d="M9 5H14.7071L18 8.29289V17H9V5ZM10 6V16H17V9H14V6H10ZM15
|
||||
6.70711L16.2929 8H15V6.70711ZM7 8H8V18H15V19H7V8Z" />
|
||||
{:else if name === "clipboard-small"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 7L17 10V16H10V7H14ZM13
|
||||
8H11V15H16V11H13V8ZM14 8.41421V10H15.5858L14 8.41421ZM8
|
||||
10H9V17H14V18H8V10Z" />
|
||||
{:else if name === "ellipsis"}
|
||||
<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" />
|
||||
{:else if name === "chevron-down"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 16.353549 9.8535548 c 0.19527 0.1952622 0.19527 0.5118432 0
|
||||
0.7071032 l -3.29289 3.2929 c -0.58579 0.58578 -1.53553 0.58578
|
||||
-2.12132 0 l -3.292894 -3.2929 c -0.195262 -0.19526 -0.195262
|
||||
-0.511841 0 -0.7071042 0.195263 -0.195262 0.511844 -0.195262
|
||||
0.707104 0 l 3.2929 3.2928942 c 0.19526 0.19526 0.51184 0.19526
|
||||
0.7071 0 l 3.2929 -3.2928932 c 0.19526 -0.195262 0.51184
|
||||
-0.195262 0.7071 0 z" />
|
||||
{:else if name === "chevron-up"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.64645 14.3536C7.45118 14.1583 7.45118 13.8417 7.64645
|
||||
13.6464L10.9393 10.3536C11.5251 9.76777 12.4749 9.76777 13.0607
|
||||
10.3536L16.3536 13.6464C16.5488 13.8417 16.5488 14.1583 16.3536
|
||||
14.3536C16.1583 14.5488 15.8417 14.5488 15.6464 14.3536L12.3536
|
||||
11.0607C12.1583 10.8654 11.8417 10.8654 11.6464 11.0607L8.35355
|
||||
14.3536C8.15829 14.5488 7.84171 14.5488 7.64645 14.3536Z" />
|
||||
{:else if name === "gear"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.23219 2.01728C9.38084
|
||||
1.97716 9.53969 2.00775 9.66279 2.10022L11.5852 3.5442C11.8032 3.53214
|
||||
12.0218 3.53214 12.2398 3.5442L14.1622 2.10022C14.2851 2.00788 14.4437
|
||||
1.97724 14.5922 2.01712C15.4415 2.24523 16.2573 2.58349 17.0188
|
||||
3.02327C17.1517 3.10003 17.242 3.23359 17.2637 3.38554L17.5851
|
||||
5.63541L20.5219 6.58028C20.6402 6.61834 20.7401 6.69907 20.8021
|
||||
6.80675C21.2406 7.56841 21.5787 8.38367 21.8077 9.23219C21.8479 9.38084
|
||||
21.8173 9.53969 21.7248 9.66279L20.2747 11.5933C20.2763 11.6489 20.278
|
||||
11.7141 20.2792 11.7842C20.2816 11.9194 20.2827 12.0847 20.2779
|
||||
12.236L21.7248 14.1622C21.8171 14.2851 21.8478 14.4437 21.8079
|
||||
14.5922C21.5798 15.4415 21.2415 16.2573 20.8017 17.0188C20.7397 17.1262
|
||||
20.64 17.2067 20.5219 17.2447L17.5851 18.1896L17.2637 20.4395C17.242 20.5916
|
||||
17.1515 20.7253 17.0183 20.8021C16.2566 21.2406 15.4413 21.5787 14.5928
|
||||
21.8077C14.4442 21.8479 14.2853 21.8173 14.1622 21.7248L12.2398
|
||||
20.2808C12.0255 20.2927 11.8107 20.2929 11.5964 20.2814L9.72748
|
||||
21.8005C9.60295 21.9017 9.4374 21.937 9.28241 21.8954C8.41326 21.6619
|
||||
7.54437 21.228 6.8062 20.8017C6.67328 20.725 6.58299 20.5914 6.56128
|
||||
20.4395L6.22012 18.0514C6.06864 17.9052 5.91981 17.7564 5.77364
|
||||
17.6049L3.38554 17.2637C3.23337 17.242 3.09966 17.1515 3.02295
|
||||
17.0183C2.58438 16.2566 2.24635 15.4413 2.01728 14.5928C1.97716 14.4442
|
||||
2.00775 14.2853 2.10022 14.1622L3.55032 12.2317C3.54868 12.1761 3.54702
|
||||
12.1109 3.54579 12.0409C3.5434 11.9056 3.54229 11.7403 3.5471
|
||||
11.5891L2.10022 9.66279C2.00788 9.53986 1.97724 9.38129 2.01712
|
||||
9.23281C2.24523 8.38352 2.58349 7.56772 3.02327 6.8062C3.10003 6.67328
|
||||
3.23359 6.58299 3.38554 6.56128L5.77364 6.22012C5.91981 6.06864 6.06864
|
||||
5.91981 6.22012 5.77364L6.56128 3.38554C6.58302 3.23337 6.67354 3.09966
|
||||
6.80675 3.02295C7.56841 2.58438 8.38367 2.24635 9.23219 2.01728ZM4.55577
|
||||
12.3686C4.56087 12.4844 4.52562 12.5983 4.45604 12.6909L3.04873
|
||||
14.5645C3.22919 15.1693 3.47157 15.7538 3.77202 16.3088L6.08634
|
||||
16.6394C6.19815 16.6554 6.30125 16.7087 6.37886 16.7908C6.59125 17.0153
|
||||
6.8097 17.2338 7.03423 17.4461C7.11628 17.5238 7.16963 17.6269 7.1856
|
||||
17.7387L7.51653 20.0552C8.09055 20.3755 8.70317 20.6699 9.30047
|
||||
20.8589L11.119 19.3808C11.2199 19.2987 11.3489 19.2592 11.4784
|
||||
19.2707C11.7672 19.2962 12.0578 19.2962 12.3466 19.2707C12.4696 19.2598
|
||||
12.5922 19.2948 12.6909 19.369L14.5645 20.7763C15.1693 20.5958 15.7538
|
||||
20.3534 16.3088 20.053L16.6394 17.7387C16.6665 17.5493 16.7992 17.392
|
||||
16.9812 17.3334L20.0293 16.3527C20.3415 15.7851 20.5919 15.1857 20.7765
|
||||
14.5648L19.369 12.6909C19.2948 12.5922 19.2598 12.4696 19.2707
|
||||
12.3467C19.2822 12.2167 19.2829 12.0017 19.2794 11.8018C19.2777 11.7056
|
||||
19.2751 11.6187 19.273 11.5559L19.2703 11.4822L19.2695 11.4626L19.2693
|
||||
11.4577L19.2692 11.4566C19.2641 11.3409 19.2994 11.2267 19.369
|
||||
11.1341L20.7763 9.26051C20.591 8.63973 20.3405 8.04027 20.029
|
||||
7.47218L16.9812 6.4916C16.7992 6.43302 16.6665 6.27569 16.6394
|
||||
6.08634L16.3087 3.77166C15.7541 3.47068 15.1696 3.22832 14.5648
|
||||
3.04851L12.6909 4.45604C12.5922 4.53018 12.4696 4.56518 12.3466
|
||||
4.55431C12.0578 4.52877 11.7672 4.52877 11.4784 4.55431C11.3554 4.56518
|
||||
11.2328 4.53018 11.1341 4.45604L9.26051 3.04873C8.65575 3.22919 8.07123
|
||||
3.47157 7.51622 3.77202L7.1856 6.08634C7.16963 6.19815 7.11628 6.30125
|
||||
7.03423 6.37886C6.8097 6.59125 6.59125 6.8097 6.37886 7.03423C6.30125
|
||||
7.11628 6.19815 7.16963 6.08634 7.1856L3.77166 7.51627C3.47068 8.07088
|
||||
3.22832 8.65536 3.04851 9.26022L4.45604 11.1341C4.53016 11.2328 4.56517
|
||||
11.3554 4.55432 11.4783C4.54285 11.6083 4.5421 11.8233 4.54563
|
||||
12.0232C4.54733 12.1194 4.54988 12.2063 4.55201 12.2691L4.55471
|
||||
12.3429L4.55551 12.3624L4.55577 12.3686ZM11.9121 8.41248C9.97912 8.41248
|
||||
8.41211 9.97948 8.41211 11.9125C8.41211 13.8455 9.97912 15.4125 11.9121
|
||||
15.4125C13.8451 15.4125 15.4121 13.8455 15.4121 11.9125C15.4121 9.97948
|
||||
13.8451 8.41248 11.9121 8.41248ZM7.41211 11.9125C7.41211 9.4272 9.42683
|
||||
7.41248 11.9121 7.41248C14.3974 7.41248 16.4121 9.4272 16.4121
|
||||
11.9125C16.4121 14.3978 14.3974 16.4125 11.9121 16.4125C9.42683 16.4125
|
||||
7.41211 14.3978 7.41211 11.9125Z" />
|
||||
{:else if name === "fork"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.34375 6.9375C7.5671
|
||||
6.9375 6.9375 7.5671 6.9375 8.34375C6.9375 9.1204 7.5671 9.75 8.34375
|
||||
9.75C9.1204 9.75 9.75 9.1204 9.75 8.34375C9.75 7.5671 9.1204 6.9375 8.34375
|
||||
6.9375ZM8.8125 10.6406C9.8823 10.4235 10.6875 9.47764 10.6875
|
||||
8.34375C10.6875 7.04933 9.63817 6 8.34375 6C7.04933 6 6 7.04933 6 8.34375C6
|
||||
9.47764 6.8052 10.4235 7.875 10.6406V11.1562C7.875 11.8999 8.18601 12.4028
|
||||
8.62101 12.7834C8.82767 12.9643 9.06032 13.1164 9.28665 13.2541C9.39277
|
||||
13.3187 9.48974 13.3756 9.5849 13.4316C9.71089 13.5056 9.83369 13.5777
|
||||
9.97031 13.6631C10.4167 13.9421 10.8179 14.2499 11.1146 14.7072C11.3641
|
||||
15.0919 11.5582 15.6119 11.6108 16.3623C10.5481 16.5849 9.75 17.5274 9.75
|
||||
18.6562C9.75 19.9507 10.7993 21 12.0938 21C13.3882 21 14.4375 19.9507
|
||||
14.4375 18.6562C14.4375 17.5274 13.6394 16.5849 12.5766 16.3623C12.6293
|
||||
15.6119 12.8234 15.0919 13.073 14.7072C13.3696 14.2499 13.7709 13.9421
|
||||
14.2172 13.6631C14.3538 13.5777 14.4766 13.5056 14.6026 13.4316C14.6978
|
||||
13.3756 14.7947 13.3187 14.9008 13.2541C15.1272 13.1164 15.3598 12.9643
|
||||
15.5665 12.7834C16.0015 12.4028 16.3125 11.8999 16.3125
|
||||
11.1562V10.6406C17.3823 10.4235 18.1875 9.47764 18.1875 8.34375C18.1875
|
||||
7.04933 17.1382 6 15.8438 6C14.5493 6 13.5 7.04933 13.5 8.34375C13.5
|
||||
9.47764 14.3052 10.4235 15.375 10.6406V11.1562C15.375 11.5845 15.2173
|
||||
11.8433 14.9491 12.0779C14.8042 12.2047 14.6267 12.3235 14.4136
|
||||
12.4532C14.3363 12.5002 14.2463 12.5532 14.1515 12.6091C14.0098 12.6926
|
||||
13.8574 12.7824 13.7203 12.8681C13.2292 13.1751 12.6929 13.5705 12.2864
|
||||
14.1971C12.2176 14.3032 12.1532 14.4147 12.0938 14.5323C12.0343 14.4147
|
||||
11.9699 14.3032 11.9011 14.1971C11.4946 13.5705 10.9583 13.1751 10.4672
|
||||
12.8681C10.3301 12.7824 10.1776 12.6926 10.036 12.6091C9.94121 12.5532
|
||||
9.85123 12.5002 9.77389 12.4532C9.56077 12.3235 9.38326 12.2047 9.23836
|
||||
12.0779C8.97024 11.8433 8.8125 11.5845 8.8125 11.1562V10.6406ZM15.8438
|
||||
9.75C16.6204 9.75 17.25 9.1204 17.25 8.34375C17.25 7.5671 16.6204 6.9375
|
||||
15.8438 6.9375C15.0671 6.9375 14.4375 7.5671 14.4375 8.34375C14.4375 9.1204
|
||||
15.0671 9.75 15.8438 9.75ZM12.0938 17.25C11.3171 17.25 10.6875 17.8796
|
||||
10.6875 18.6562C10.6875 19.4329 11.3171 20.0625 12.0938 20.0625C12.8704
|
||||
20.0625 13.5 19.4329 13.5 18.6562C13.5 17.8796 12.8704 17.25 12.0938
|
||||
17.25Z" />
|
||||
{:else if name === "github"}
|
||||
<path
|
||||
d="M12 4C7.58 4 4 7.67295 4 12.2031C4 15.8282 6.292 18.9023 9.47
|
||||
19.9858C9.87 20.0631 10.0167 19.8095 10.0167 19.5914C10.0167 19.3966 10.01
|
||||
18.8805 10.0067 18.1969C7.78133 18.6918 7.312 17.0963 7.312 17.0963C6.948
|
||||
16.1495 6.422 15.8966 6.422 15.8966C5.69733 15.388 6.478 15.3982 6.478
|
||||
15.3982C7.28133 15.4557 7.70333 16.2432 7.70333 16.2432C8.41667 17.4975
|
||||
9.576 17.1352 10.0333 16.9254C10.1053 16.3949 10.3113 16.0333 10.54
|
||||
15.8282C8.76333 15.6231 6.896 14.9177 6.896 11.7745C6.896 10.879 7.206
|
||||
10.1476 7.71933 9.57334C7.62933 9.36621 7.35933 8.53222 7.78933
|
||||
7.40224C7.78933 7.40224 8.45933 7.18213 9.98933 8.24306C10.6293 8.06054
|
||||
11.3093 7.97031 11.9893 7.96621C12.6693 7.97031 13.3493 8.06054 13.9893
|
||||
8.24306C15.5093 7.18213 16.1793 7.40224 16.1793 7.40224C16.6093 8.53222
|
||||
16.3393 9.36621 16.2593 9.57334C16.7693 10.1476 17.0793 10.879 17.0793
|
||||
11.7745C17.0793 14.9259 15.2093 15.6197 13.4293 15.8214C13.7093 16.0675
|
||||
13.9693 16.5706 13.9693 17.339C13.9693 18.4368 13.9593 19.3186 13.9593
|
||||
19.5852C13.9593 19.8006 14.0993 20.0569 14.5093 19.9749C17.71 18.8989 20
|
||||
15.8227 20 12.2031C20 7.67295 16.418 4 12 4" />
|
||||
{:else if name === "moon"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.75 1.5C16.1642 1.5
|
||||
16.5 1.83579 16.5 2.25V3H17.25C17.6642 3 18 3.33579 18 3.75C18 4.16421
|
||||
17.6642 4.5 17.25 4.5H16.5V5.25C16.5 5.66421 16.1642 6 15.75 6C15.3358 6 15
|
||||
5.66421 15 5.25V4.5H14.25C13.8358 4.5 13.5 4.16421 13.5 3.75C13.5 3.33579
|
||||
13.8358 3 14.25 3H15V2.25C15 1.83579 15.3358 1.5 15.75 1.5ZM10.2246
|
||||
3.15456C10.4159 3.34623 10.489 3.62613 10.4159 3.88688C9.65641 6.59529
|
||||
10.4385 9.58344 12.4276 11.5724C14.4166 13.5615 17.4047 14.3436 20.1131
|
||||
13.5841C20.3739 13.511 20.6538 13.5841 20.8454 13.7754C21.0371 13.9667
|
||||
21.1108 14.2465 21.0382 14.5074C19.1394 21.3293 10.2649 23.5102 5.37735
|
||||
18.6226C0.393755 13.639 2.70287 4.85168 9.49264 2.96184C9.75353 2.88923
|
||||
10.0333 2.96289 10.2246 3.15456ZM8.69851 4.84862C3.95753 7.06376 2.57424
|
||||
13.6982 6.43801 17.562C10.2273 21.3512 16.9232 20.041 19.1499
|
||||
15.3017C16.3194 15.6466 13.4079 14.6741 11.3669 12.6331C9.32556 10.5918
|
||||
8.35305 7.67963 8.69851 4.84862ZM20.25 5.25C20.6642 5.25 21 5.58579 21
|
||||
6V7.5H22.5C22.9142 7.5 23.25 7.83579 23.25 8.25C23.25 8.66421 22.9142 9
|
||||
22.5 9H21V10.5C21 10.9142 20.6642 11.25 20.25 11.25C19.8358 11.25 19.5
|
||||
10.9142 19.5 10.5V9H18C17.5858 9 17.25 8.66421 17.25 8.25C17.25 7.83579
|
||||
17.5858 7.5 18 7.5H19.5V6C19.5 5.58579 19.8358 5.25 20.25 5.25Z" />
|
||||
{:else if name === "url"}
|
||||
<path
|
||||
d="M18.7566 11.2493L15.7531 14.2518C14.0953 15.9107 11.4059 15.9107
|
||||
9.74803 14.2518C9.48676 13.9916 9.28252 13.6982 9.10313 13.3954L10.4987
|
||||
11.9999C10.565 11.933 10.6469 11.8947 10.7252 11.8496C10.8216 12.1793
|
||||
10.9901 12.4914 11.2493 12.7505C12.0772 13.5789 13.4245 13.5779 14.2518
|
||||
12.7505L17.2543 9.74802C18.0827 8.91963 18.0827 7.57285 17.2543
|
||||
6.74499C16.427 5.91713 15.0802 5.91713 14.2518 6.74499L13.1839
|
||||
7.81391C12.3177 7.47644 11.3841 7.38573 10.4753 7.51894L12.7505
|
||||
5.24373C14.4094 3.58542 17.0977 3.58542 18.7566 5.24373C20.4145 6.90211
|
||||
20.4145 9.591 18.7566 11.2493ZM10.8164 16.1865L9.74803 17.2554C8.92016
|
||||
18.0828 7.57284 18.0828 6.74497 17.2554C5.9171 16.427 5.9171 15.0802
|
||||
6.74497 14.2519L9.74803 11.2493C10.5764 10.421 11.9227 10.421 12.7506
|
||||
11.2493C13.0092 11.508 13.1777 11.8201 13.2752 12.1493C13.354 12.1036
|
||||
13.4349 12.0663 13.5012 12L14.8967 10.605C14.7184 10.3012 14.5131 10.0088
|
||||
14.2518 9.74809C12.594 8.08978 9.90462 8.08978 8.24627 9.74809L5.24374
|
||||
12.7506C3.58542 14.4094 3.58542 17.0978 5.24374 18.7566C6.90207 20.4145
|
||||
9.59097 20.4145 11.2493 18.7566L13.5251 16.4809C12.6158 16.6146 11.6822
|
||||
16.5234 10.8164 16.1865Z" />
|
||||
{:else if name === "checkmark"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M18.2941 6.59564C18.5174 6.75806 18.5668 7.07076 18.4044 7.29409L11.781
|
||||
16.4012C10.8775 17.6436 9.07768 17.7848 7.99142 16.6985L5.64645 14.3536C5.45118
|
||||
14.1583 5.45118 13.8417 5.64645 13.6465C5.84171 13.4512 6.15829 13.4512 6.35355
|
||||
13.6465L8.69852 15.9914C9.35028 16.6432 10.4302 16.5584 10.9723 15.813L17.5956
|
||||
6.70592C17.7581 6.48259 18.0708 6.43322 18.2941 6.59564Z" />
|
||||
{:else if name === "sun"}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 0.75C12.4142 0.75
|
||||
12.75 1.08579 12.75 1.5V3.375C12.75 3.78921 12.4142 4.125 12 4.125C11.5858
|
||||
4.125 11.25 3.78921 11.25 3.375V1.5C11.25 1.08579 11.5858 0.75 12
|
||||
0.75ZM4.04467 4.04467C4.33756 3.75178 4.81244 3.75178 5.10533
|
||||
4.04467L6.42721 5.36655C6.7201 5.65944 6.7201 6.13431 6.42721
|
||||
6.42721C6.13431 6.7201 5.65944 6.7201 5.36655 6.42721L4.04467
|
||||
5.10533C3.75178 4.81244 3.75178 4.33756 4.04467 4.04467ZM19.9553
|
||||
4.04467C20.2482 4.33756 20.2482 4.81244 19.9553 5.10533L18.6335
|
||||
6.42721C18.3406 6.7201 17.8657 6.7201 17.5728 6.42721C17.2799 6.13431
|
||||
17.2799 5.65944 17.5728 5.36655L18.8947 4.04467C19.1876 3.75178 19.6624
|
||||
3.75178 19.9553 4.04467ZM12 7.125C9.30761 7.125 7.125 9.30761 7.125
|
||||
12C7.125 14.6924 9.30761 16.875 12 16.875C14.6924 16.875 16.875 14.6924
|
||||
16.875 12C16.875 9.30761 14.6924 7.125 12 7.125ZM5.625 12C5.625 8.47918
|
||||
8.47918 5.625 12 5.625C15.5208 5.625 18.375 8.47918 18.375 12C18.375
|
||||
15.5208 15.5208 18.375 12 18.375C8.47918 18.375 5.625 15.5208 5.625
|
||||
12ZM0.75 12C0.75 11.5858 1.08579 11.25 1.5 11.25H3.375C3.78921 11.25 4.125
|
||||
11.5858 4.125 12C4.125 12.4142 3.78921 12.75 3.375 12.75H1.5C1.08579 12.75
|
||||
0.75 12.4142 0.75 12ZM19.875 12C19.875 11.5858 20.2108 11.25 20.625
|
||||
11.25H22.5C22.9142 11.25 23.25 11.5858 23.25 12C23.25 12.4142 22.9142 12.75
|
||||
22.5 12.75H20.625C20.2108 12.75 19.875 12.4142 19.875 12ZM6.42721
|
||||
17.5728C6.7201 17.8657 6.7201 18.3406 6.42721 18.6335L5.10533
|
||||
19.9553C4.81244 20.2482 4.33756 20.2482 4.04467 19.9553C3.75178 19.6624
|
||||
3.75178 19.1876 4.04467 18.8947L5.36655 17.5728C5.65944 17.2799 6.13431
|
||||
17.2799 6.42721 17.5728ZM17.5728 17.5728C17.8657 17.2799 18.3406 17.2799
|
||||
18.6335 17.5728L19.9553 18.8947C20.2482 19.1876 20.2482 19.6624 19.9553
|
||||
19.9553C19.6624 20.2482 19.1876 20.2482 18.8947 19.9553L17.5728
|
||||
18.6335C17.2799 18.3406 17.2799 17.8657 17.5728 17.5728ZM12 19.875C12.4142
|
||||
19.875 12.75 20.2108 12.75 20.625V22.5C12.75 22.9142 12.4142 23.25 12
|
||||
23.25C11.5858 23.25 11.25 22.9142 11.25 22.5V20.625C11.25 20.2108 11.5858
|
||||
19.875 12 19.875Z" />
|
||||
{:else if name === "twitter"}
|
||||
<path
|
||||
d="M19.9687 7.54849C19.3697 7.81214 18.7351 7.98617 18.0853
|
||||
8.06498C18.7694 7.65395 19.2816 7.00936 19.5273 6.25025C18.8933 6.62013
|
||||
18.1907 6.88937 17.4427 7.03932C16.9492 6.51179 16.2952 6.1619 15.5824
|
||||
6.04399C14.8696 5.92608 14.1378 6.04675 13.5006 6.38727C12.8635 6.72778
|
||||
12.3566 7.26908 12.0587 7.92711C11.7608 8.58514 11.6886 9.32307 11.8533
|
||||
10.0263C9.12667 9.8977 6.71133 8.58814 5.09333 6.61013C4.7992 7.10984
|
||||
4.64578 7.67979 4.64933 8.25958C4.64933 9.3992 5.22933 10.4009 6.108
|
||||
10.9893C5.58724 10.9728 5.07798 10.832 4.62267 10.5788V10.6188C4.62237
|
||||
11.3761 4.88418 12.1103 5.36367 12.6966C5.84316 13.283 6.51081 13.6854
|
||||
7.25333 13.8357C6.7722 13.9646 6.26828 13.984 5.77867 13.8924C5.98941
|
||||
14.5442 6.39844 15.1139 6.94868 15.5222C7.49891 15.9304 8.1629 16.1567
|
||||
8.848 16.1696C7.68769 17.0799 6.25498 17.574 4.78 17.5725C4.52 17.5725
|
||||
4.26067 17.5571 4 17.5278C5.50381 18.4904 7.25234 19.0013 9.038 19C15.0733
|
||||
19 18.37 14.0043 18.37 9.67978C18.37 9.53982 18.37 9.39987 18.36
|
||||
9.25992C19.004 8.79665 19.5595 8.22147 20 7.56182L19.9687 7.54849Z" />
|
||||
{:else}
|
||||
{unreachable(name)}
|
||||
{/if}
|
||||
</svg>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Route } from "@app/lib/router/definitions";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { push, routeToPath } from "@app/lib/router";
|
||||
|
||||
export let route: Route;
|
||||
export let title: string | null = null;
|
||||
export let id: string | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
click: never;
|
||||
}>();
|
||||
|
||||
function onClick(): void {
|
||||
push(route);
|
||||
dispatch("click");
|
||||
}
|
||||
</script>
|
||||
|
||||
<a on:click|preventDefault={onClick} {title} {id} href={routeToPath(route)}>
|
||||
<slot />
|
||||
</a>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
|
||||
type Item = $$Generic;
|
||||
|
||||
export let complete = false;
|
||||
export let items: Item[];
|
||||
export let query: () => Promise<Item[]>;
|
||||
|
||||
// Used to handle the display of the trigger to load more items, according to the current loading state.
|
||||
let loading = false;
|
||||
|
||||
const transitionParams = { duration: 200 };
|
||||
|
||||
const fetchMore = async () => {
|
||||
loading = true;
|
||||
const response = await query();
|
||||
|
||||
if (response.length > 0) {
|
||||
items = [...items, ...response];
|
||||
} else {
|
||||
complete = true;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.more {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<slot name="list" {items} />
|
||||
{#if !complete}
|
||||
<slot name="more" {fetchMore} {loading}>
|
||||
{#if loading}
|
||||
<div class="more" transition:fade|local={transitionParams}>
|
||||
<Loading small />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="more" transition:fade|local={transitionParams}>
|
||||
<Button
|
||||
variant="foreground"
|
||||
waiting={loading}
|
||||
disabled={loading}
|
||||
on:click={fetchMore}>
|
||||
More
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</slot>
|
||||
{/if}
|
||||
@@ -0,0 +1,165 @@
|
||||
<script lang="ts">
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
export let small = false;
|
||||
export let center = false;
|
||||
export let fadeIn = false;
|
||||
export let margins = false;
|
||||
export let condensed = false;
|
||||
export let noDelay = false;
|
||||
|
||||
let show: boolean = false;
|
||||
|
||||
if (noDelay) {
|
||||
show = true;
|
||||
} else {
|
||||
debounce(() => (show = true), 200)();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin: auto 0;
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
cursor: wait;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
}
|
||||
.spinner.center {
|
||||
margin: auto auto;
|
||||
}
|
||||
.spinner.margins {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.spinner > div {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: var(--color-secondary);
|
||||
border-radius: var(--border-radius-round);
|
||||
display: inline-block;
|
||||
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.spinner.condensed > div {
|
||||
-webkit-animation: sk-bouncedelay-condensed 0.7s infinite linear both;
|
||||
animation: sk-bouncedelay-condensed 0.7s infinite linear both;
|
||||
}
|
||||
|
||||
.spinner.small > div {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
.spinner.small {
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
.spinner .bounce1 {
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.spinner .bounce2 {
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bouncedelay-condensed {
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: scale(0.2);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sk-bouncedelay-condensed {
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: scale(0.2);
|
||||
transform: scale(0.2);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bouncedelay {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
40% {
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sk-bouncedelay {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
40% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 1.5s;
|
||||
animation-timing-function: ease-in;
|
||||
-webkit-animation: fadeIn 1.5s;
|
||||
-webkit-animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if show}
|
||||
<div class="container">
|
||||
<div
|
||||
class="spinner"
|
||||
class:fade-in={fadeIn}
|
||||
class:small
|
||||
class:center
|
||||
class:margins
|
||||
class:condensed>
|
||||
<div class="bounce1" style="background-color: var(--color-secondary)" />
|
||||
{#if !condensed}
|
||||
<div class="bounce2" style="background-color: var(--color-secondary)" />
|
||||
<div class="bounce3" style="background-color: var(--color-secondary)" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,307 @@
|
||||
<script lang="ts">
|
||||
import type * as proj from "@app/lib/project";
|
||||
|
||||
import dompurify from "dompurify";
|
||||
import matter from "@radicle/gray-matter";
|
||||
import { marked } from "marked";
|
||||
import { onMount } from "svelte";
|
||||
import { toDom } from "hast-util-to-dom";
|
||||
|
||||
import { base } from "@app/lib/router";
|
||||
import { highlight } from "@app/lib/syntax";
|
||||
import {
|
||||
markdownExtensions as extensions,
|
||||
renderer,
|
||||
getImageMime,
|
||||
isUrl,
|
||||
twemoji,
|
||||
scrollIntoView,
|
||||
} from "@app/lib/utils";
|
||||
|
||||
export let content: string;
|
||||
export let doc = matter(content);
|
||||
export let getImage: (path: string) => Promise<proj.MaybeBlob>;
|
||||
export let hash: string | null = null;
|
||||
|
||||
const frontMatter = Object.entries(doc.data).filter(
|
||||
([, val]) => typeof val === "string" || typeof val === "number",
|
||||
);
|
||||
marked.use({ extensions, renderer });
|
||||
|
||||
let container: HTMLElement;
|
||||
|
||||
const render = (content: string): string =>
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
dompurify.sanitize(marked.parse(content), { SANITIZE_DOM: false });
|
||||
|
||||
onMount(async () => {
|
||||
// Don't underline <a> tags that contain images.
|
||||
const elems = container.querySelectorAll("a");
|
||||
|
||||
for (const e of elems) {
|
||||
if (e.firstElementChild instanceof HTMLImageElement) {
|
||||
e.classList.add("no-underline");
|
||||
}
|
||||
}
|
||||
|
||||
if (hash) scrollIntoView(hash);
|
||||
|
||||
// Iterate over all images, and fetch their data from the API, then
|
||||
// replace the source with a Data-URL. We do this due to the absence
|
||||
// of a static file server.
|
||||
for (const i of container.querySelectorAll("img")) {
|
||||
const path = i.getAttribute("src");
|
||||
|
||||
// Make sure the source isn't a URL before trying to fetch it from the repo
|
||||
if (path && !isUrl(path) && !path.startsWith(`${base}twemoji`)) {
|
||||
getImage(path).then(blob => {
|
||||
if (blob?.content) {
|
||||
const mime = getImageMime(path);
|
||||
if (mime) {
|
||||
i.setAttribute("src", `data:${mime};base64,${blob.content}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces code blocks in the background with highlighted code.
|
||||
const prefix = "language-";
|
||||
const nodes = Array.from(document.body.querySelectorAll("pre code"));
|
||||
|
||||
const treeChanges: Promise<void>[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
const className = Array.from(node.classList).find(name =>
|
||||
name.startsWith(prefix),
|
||||
);
|
||||
if (!className) continue;
|
||||
|
||||
treeChanges.push(
|
||||
highlight(node.textContent ?? "", className.slice(prefix.length))
|
||||
.then(tree => {
|
||||
if (tree) {
|
||||
node.replaceChildren(toDom(tree, { fragment: true }));
|
||||
}
|
||||
})
|
||||
.catch(e => console.warn("Not able to highlight code block", e)),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.allSettled(treeChanges);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.front-matter {
|
||||
font-size: var(--font-size-tiny);
|
||||
font-family: var(--font-family-monospace);
|
||||
color: var(--color-foreground);
|
||||
border: 1px dashed var(--color-foreground-4);
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.front-matter table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.front-matter table td {
|
||||
padding: 0.125rem 1rem;
|
||||
}
|
||||
.front-matter table td:first-child {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown :global(h1),
|
||||
.markdown :global(h2),
|
||||
.markdown :global(h3),
|
||||
.markdown :global(h4),
|
||||
.markdown :global(h5),
|
||||
.markdown :global(h6) {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.markdown :global(h1) {
|
||||
font-size: calc(var(--font-size-huge) * 0.75);
|
||||
font-weight: var(--font-weight-medium);
|
||||
padding: 1rem 0 0.5rem 0;
|
||||
margin: 0 0 0.75rem;
|
||||
border-bottom: 1px solid var(--color-foreground-4);
|
||||
}
|
||||
|
||||
.markdown :global(h2) {
|
||||
font-size: var(--font-size-medium);
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding: 0.25rem 0;
|
||||
margin: 2rem 0 0.5rem;
|
||||
border-bottom: 1px dashed var(--color-foreground-4);
|
||||
}
|
||||
|
||||
.markdown :global(h3) {
|
||||
font-size: calc(var(--font-size-medium) * 0.9);
|
||||
font-weight: var(--font-weight-medium);
|
||||
padding: 0.5rem 0;
|
||||
margin: 1rem 0 0.25rem;
|
||||
}
|
||||
|
||||
.markdown :global(h4) {
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-regular);
|
||||
padding: 0.5rem 0;
|
||||
margin: 1rem 0 0.125rem;
|
||||
}
|
||||
|
||||
.markdown :global(h5),
|
||||
.markdown :global(h6) {
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-small);
|
||||
padding: 0.35rem 0;
|
||||
margin: 1rem 0 0.125rem;
|
||||
}
|
||||
|
||||
.markdown :global(h6) {
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
|
||||
.markdown :global(p) {
|
||||
line-height: 1.625;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.markdown :global(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown :global(blockquote) {
|
||||
border-left: 0.3rem solid var(--color-foreground-4);
|
||||
padding: 0 0 0 1rem;
|
||||
margin: 0 0 0.625rem 0;
|
||||
}
|
||||
|
||||
.markdown :global(strong) {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.markdown :global(img) {
|
||||
border-style: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown :global(code) {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-regular);
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-foreground-2);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
}
|
||||
|
||||
.markdown :global(pre code) {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown :global(pre) {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-regular);
|
||||
background-color: var(--color-foreground-2);
|
||||
padding: 1rem !important;
|
||||
border-radius: var(--border-radius-small);
|
||||
margin: 1rem 0;
|
||||
overflow: scroll;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.markdown :global(pre::-webkit-scrollbar) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown :global(a),
|
||||
.markdown :global(a > code) {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
.markdown :global(a) {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--color-foreground-6);
|
||||
}
|
||||
.markdown :global(a.no-underline) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.markdown :global(hr) {
|
||||
height: 0;
|
||||
margin: 1rem 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--color-foreground-4);
|
||||
}
|
||||
|
||||
.markdown :global(ol) {
|
||||
line-height: 1.625;
|
||||
list-style-type: decimal;
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.markdown :global(ul) {
|
||||
line-height: 1.625;
|
||||
list-style-type: inherit;
|
||||
padding-left: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.markdown :global(table) {
|
||||
margin: 1.5rem 0;
|
||||
border-collapse: collapse;
|
||||
border-radius: 0.5rem;
|
||||
border-style: hidden;
|
||||
box-shadow: 0 0 0 1px var(--color-foreground-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown :global(td) {
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
border: 1px solid var(--color-foreground-4);
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.markdown :global(tr:nth-child(even)) {
|
||||
background-color: var(--color-foreground-2);
|
||||
}
|
||||
.markdown :global(th) {
|
||||
text-align: center;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.markdown :global(*:first-child:not(pre)) {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.markdown :global(*:first-child) {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.markdown :global(dl dt) {
|
||||
font-style: italic;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.markdown :global(dl dd) {
|
||||
margin: 0 0 0 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if frontMatter.length > 0}
|
||||
<div class="front-matter">
|
||||
<table>
|
||||
{#each frontMatter as [key, val]}
|
||||
<tr>
|
||||
<td><span class="txt-bold">{key}</span></td>
|
||||
<td>{val}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="markdown" bind:this={container} use:twemoji>
|
||||
{@html render(doc.content)}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
export let error = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.message {
|
||||
padding: 1rem;
|
||||
}
|
||||
.message-error {
|
||||
color: var(--color-negative);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--glow-error);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="message" class:message-error={error}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
export let floating = false;
|
||||
export let error = false;
|
||||
export let subtle = false;
|
||||
export let small = false;
|
||||
export let narrow = false;
|
||||
export let center = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.floating,
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.floating {
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.overlay {
|
||||
z-index: 200;
|
||||
background-color: #00000075;
|
||||
}
|
||||
.modal {
|
||||
padding: 2rem 3rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
background: var(--color-background);
|
||||
box-shadow: var(--elevation-high);
|
||||
min-width: 480px;
|
||||
max-width: 760px;
|
||||
text-align: center;
|
||||
}
|
||||
.narrow {
|
||||
max-width: 600px;
|
||||
}
|
||||
.subtle {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: radial-gradient(var(--glow) 0%, transparent 70%);
|
||||
}
|
||||
.subtle.error {
|
||||
background: radial-gradient(var(--glow-error) 0%, transparent 70%);
|
||||
}
|
||||
.title {
|
||||
color: var(--color-foreground);
|
||||
font-size: var(--font-size-medium);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 2.625rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-regular);
|
||||
max-width: 90%;
|
||||
margin: 0 auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.body {
|
||||
color: var(--color-foreground);
|
||||
font-size: var(--font-size-regular);
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
.actions {
|
||||
padding-top: 0.5rem;
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.small .subtitle {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
.error {
|
||||
box-shadow: var(--elevation-high-negative);
|
||||
border-color: var(--color-negative);
|
||||
}
|
||||
|
||||
.error > * {
|
||||
color: var(--color-negative);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.modal {
|
||||
width: 90%;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if floating}
|
||||
<div class="overlay" />
|
||||
{/if}
|
||||
|
||||
<div class:floating class:layout-centered={!center}>
|
||||
<div class="modal" class:subtle class:narrow class:small class:error>
|
||||
<div class="title">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
<div class="subtitle">
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
{#if $$slots.body && !small}
|
||||
<div class="body">
|
||||
<slot name="body" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,89 +0,0 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import Modal from "@app/Modal.svelte";
|
||||
import { formatRadicleUrn, getSeedEmoji, twemoji } from "@app/utils";
|
||||
import type { Wallet } from "@app/wallet";
|
||||
import Address from "@app/Address.svelte";
|
||||
import Button from "@app/Button.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { ProjectsAndProfiles } from "@app/Search.svelte";
|
||||
import Link from "@app/router/Link.svelte";
|
||||
|
||||
export let query: string;
|
||||
export let results: ProjectsAndProfiles;
|
||||
export let wallet: Wallet;
|
||||
|
||||
const dispatch = createEventDispatcher<{ close: never }>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.results {
|
||||
text-align: left;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.urn {
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:window on:click={() => dispatch("close")} />
|
||||
|
||||
<Modal center floating>
|
||||
<span slot="title" use:twemoji>️🔍</span>
|
||||
<span slot="subtitle">
|
||||
<p class="txt-highlight txt-medium">
|
||||
<span class="txt-bold">
|
||||
Results for <q>{query}</q>
|
||||
</span>
|
||||
</p>
|
||||
</span>
|
||||
<span class="results" slot="body">
|
||||
{#if results.projects.length > 0}
|
||||
<p class="txt-highlight txt-medium">Projects</p>
|
||||
<ul>
|
||||
{#each results.projects as project}
|
||||
<li>
|
||||
<Link
|
||||
route={{
|
||||
resource: "projects",
|
||||
params: {
|
||||
view: { resource: "tree" },
|
||||
seed: project.seed.host,
|
||||
urn: project.info.urn,
|
||||
},
|
||||
}}>
|
||||
<span title={project.seed.host}>
|
||||
<span>
|
||||
{getSeedEmoji(project.seed.host)} {project.info.name}
|
||||
</span>
|
||||
<span class="urn">
|
||||
{formatRadicleUrn(project.info.urn)}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if results.profiles.length > 0}
|
||||
<p class="txt-highlight txt-medium">ENS names</p>
|
||||
<ul>
|
||||
{#each results.profiles as profile}
|
||||
<li>
|
||||
<Address address={profile.address} {profile} {wallet} resolve />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</span>
|
||||
<span slot="actions">
|
||||
<Button variant="foreground" on:click={() => dispatch("close")}>
|
||||
Close
|
||||
</Button>
|
||||
</span>
|
||||
</Modal>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import * as router from "@app/lib/router";
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Modal from "@app/components/Modal.svelte";
|
||||
|
||||
export let title = "";
|
||||
export let subtitle = "";
|
||||
</script>
|
||||
|
||||
<Modal subtle>
|
||||
<span slot="title" use:twemoji>🏜️</span>
|
||||
<span slot="body">
|
||||
<p class="txt-medium txt-highlight">
|
||||
<span class="txt-bold">{title}</span>
|
||||
</p>
|
||||
<p>{subtitle}</p>
|
||||
</span>
|
||||
<span slot="actions">
|
||||
<Button variant="foreground" on:click={router.pop}>Back</Button>
|
||||
</span>
|
||||
</Modal>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
export let emoji: string;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.placeholder {
|
||||
background-color: var(--color-foreground-1);
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--color-foreground-5);
|
||||
overflow-x: hidden;
|
||||
padding: 1rem 1rem 2rem 1rem;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.placeholder header {
|
||||
padding: 1rem 0;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.placeholder .emoji {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="placeholder">
|
||||
<header>
|
||||
<div class="emoji txt-large" use:twemoji>{emoji}</div>
|
||||
<slot name="title" />
|
||||
</header>
|
||||
<slot name="body" />
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { ProjectsParams } from "@app/lib/router/definitions";
|
||||
import { updateProjectRoute, projectLinkHref } from "@app/lib/router";
|
||||
|
||||
export let projectParams: Partial<ProjectsParams>;
|
||||
export let id: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<a
|
||||
{id}
|
||||
on:click|preventDefault={() => {
|
||||
updateProjectRoute(projectParams);
|
||||
}}
|
||||
href={projectLinkHref(projectParams)}>
|
||||
<slot />
|
||||
</a>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { parseRadicleId, toClipboard, twemoji } from "@app/lib/utils";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
|
||||
export let urn: string;
|
||||
|
||||
let copied = false;
|
||||
|
||||
function copy() {
|
||||
toClipboard(urn).then(() => {
|
||||
copied = true;
|
||||
setTimeout(() => {
|
||||
copied = false;
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.urn {
|
||||
display: inline-flex;
|
||||
font-size: var(--font-size-regular);
|
||||
line-height: 2rem;
|
||||
color: var(--color-foreground-6);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.icon {
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.urn > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layout-desktop">
|
||||
<div class="urn">
|
||||
<span class="icon" use:twemoji>🌱</span>
|
||||
<span class="txt-faded">rad:git:</span>
|
||||
<span>{parseRadicleId(urn)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="foreground" size="small" disabled={copied} on:click={copy}>
|
||||
{#if copied}
|
||||
Copy ✓
|
||||
{:else}
|
||||
Copy
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import type { Seed } from "@app/lib/seed";
|
||||
|
||||
import Clipboard from "@app/components/Clipboard.svelte";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import {
|
||||
formatSeedAddress,
|
||||
formatSeedId,
|
||||
formatSeedHost,
|
||||
twemoji,
|
||||
} from "@app/lib/utils";
|
||||
|
||||
export let seed: Seed;
|
||||
export let port: number;
|
||||
export let full = false;
|
||||
|
||||
const seedHost = seed.api.port
|
||||
? `${seed.api.host}:${seed.api.port}`
|
||||
: `${formatSeedHost(seed.api.host)}`;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.seed-address {
|
||||
display: inline-flex;
|
||||
font-size: var(--font-size-regular);
|
||||
line-height: 2rem;
|
||||
color: var(--color-foreground-6);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.seed-icon {
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.seed-address > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="seed-address">
|
||||
<span class="seed-icon" use:twemoji>{seed.emoji}</span>
|
||||
{#if full}
|
||||
<span>
|
||||
<Link
|
||||
route={{
|
||||
resource: "seeds",
|
||||
params: { host: formatSeedHost(seedHost) },
|
||||
}}>
|
||||
<span class="txt-link">{formatSeedId(seed.id)}@{seed.host}</span>
|
||||
</Link>
|
||||
</span>
|
||||
<span class="txt-faded">:{port}</span>
|
||||
{:else}
|
||||
<span>
|
||||
<Link
|
||||
route={{
|
||||
resource: "seeds",
|
||||
params: { host: seedHost },
|
||||
}}>
|
||||
<span class="txt-link">{formatSeedHost(seedHost)}</span>
|
||||
</Link>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<Clipboard
|
||||
small
|
||||
text={full ? formatSeedAddress(seed.id, seed.host, port) : seed.host} />
|
||||
</div>
|
||||
<div class="layout-desktop" />
|
||||
@@ -1,121 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { CodeFont } from "@app/appearance";
|
||||
|
||||
import Icon from "@app/Icon.svelte";
|
||||
import ThemeToggle from "@app/ThemeToggle.svelte";
|
||||
import { codeFont, storeCodeFont } from "@app/appearance";
|
||||
import { codeFonts } from "@app/appearance";
|
||||
import { quadIn } from "svelte/easing";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
let showFonts = false;
|
||||
|
||||
$: document.documentElement.setAttribute("data-codefont", $codeFont);
|
||||
|
||||
const switchFont = (font: CodeFont) => {
|
||||
codeFont.set(font);
|
||||
storeCodeFont(font);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: 5rem;
|
||||
right: 1.5rem;
|
||||
width: 16.5rem;
|
||||
background: var(--color-foreground-1);
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--color-foreground-6);
|
||||
box-shadow: var(--elevation-low);
|
||||
}
|
||||
.dropdown:hover :last-child {
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
.item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 2.5rem;
|
||||
padding: 0 0.8rem;
|
||||
font-weight: 600;
|
||||
line-height: 2.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
.item:first-of-type {
|
||||
border-bottom: 1px solid var(--color-foreground-3);
|
||||
}
|
||||
.selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
.fonts {
|
||||
width: 100%;
|
||||
}
|
||||
.fonts > .item {
|
||||
border-bottom: none;
|
||||
}
|
||||
.font {
|
||||
color: var(--color-foreground-5);
|
||||
cursor: pointer;
|
||||
}
|
||||
.font:last-of-type {
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
.selector:hover {
|
||||
background-color: var(--color-foreground-3);
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
.font:hover {
|
||||
background-color: var(--color-foreground-3);
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.active,
|
||||
.active:hover {
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="dropdown">
|
||||
<div class="item">
|
||||
<span>Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="item selector"
|
||||
on:click|stopPropagation={() => (showFonts = !showFonts)}>
|
||||
<div>Code font</div>
|
||||
<Icon name={`chevron-${showFonts ? "up" : "down"}`} />
|
||||
</div>
|
||||
{#if showFonts}
|
||||
<div
|
||||
class="fonts"
|
||||
transition:slide|local={{ duration: 150, easing: quadIn }}>
|
||||
{#each codeFonts as font}
|
||||
{@const isSelectedFont = $codeFont === font.storedName}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:click={() => switchFont(font.storedName)}
|
||||
class="item font"
|
||||
class:active={isSelectedFont}
|
||||
style:font-family={font.fontFamily}>
|
||||
{font.displayName}
|
||||
{#if isSelectedFont}
|
||||
<Icon name="checkmark" />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
|
||||
export let name: string | undefined = undefined;
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let value: string | undefined = undefined;
|
||||
|
||||
export let variant: "regular" | "dashed" = "regular";
|
||||
|
||||
export let autofocus: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
export let loading: boolean = false;
|
||||
export let valid: boolean = false;
|
||||
export let validationMessage: string | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
submit: never;
|
||||
}>();
|
||||
|
||||
let rightContainerWidth: number;
|
||||
let inputElement: HTMLInputElement | undefined = undefined;
|
||||
|
||||
onMount(() => {
|
||||
if (autofocus && inputElement) {
|
||||
// We set preventScroll to true for Svelte animations to work.
|
||||
inputElement.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
dispatch("submit");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 2.5rem;
|
||||
}
|
||||
input {
|
||||
background: transparent;
|
||||
border-radius: var(--border-radius-round);
|
||||
color: var(--color-foreground);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
height: var(--button-regular-height);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
input::placeholder {
|
||||
color: var(--color-secondary);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
input[disabled] {
|
||||
color: var(--color-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.regular {
|
||||
border: 1px solid var(--color-secondary);
|
||||
font-size: var(--font-size-regular);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.dashed {
|
||||
border: 1px dashed var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
padding: 0.5rem 1.25rem;
|
||||
}
|
||||
.right-container {
|
||||
color: var(--color-secondary);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--button-regular-height);
|
||||
padding-right: 1rem;
|
||||
padding-left: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.validation-message {
|
||||
color: var(--color-negative);
|
||||
font-size: var(--font-size-small);
|
||||
margin-left: 1rem;
|
||||
position: relative;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.validation-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.key-hint {
|
||||
border-radius: 0.25rem;
|
||||
color: var(--color-secondary);
|
||||
background-color: var(--color-secondary-2);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="validation-wrapper">
|
||||
<input
|
||||
class:regular={variant === "regular"}
|
||||
class:dashed={variant === "dashed"}
|
||||
style:padding-right={rightContainerWidth
|
||||
? `${rightContainerWidth}px`
|
||||
: "auto"}
|
||||
bind:this={inputElement}
|
||||
type="text"
|
||||
{name}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
bind:value
|
||||
on:input
|
||||
on:keydown|stopPropagation={handleKeydown}
|
||||
on:click
|
||||
on:change />
|
||||
|
||||
<div class="right-container" bind:clientWidth={rightContainerWidth}>
|
||||
{#if $$slots.right}
|
||||
<slot name="right" />
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<Loading small noDelay />
|
||||
{/if}
|
||||
|
||||
{#if valid && !loading}
|
||||
<div class="key-hint">⏎</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if validationMessage}
|
||||
<div class="validation-message">
|
||||
{validationMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
// Is not as good as crypto.randomUUID() but we need some kind of fallback
|
||||
const id = self.crypto.randomUUID
|
||||
? self.crypto.randomUUID()
|
||||
: new Date().getTime().toString();
|
||||
|
||||
export let checked: boolean;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.toggle input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle label {
|
||||
background-color: var(--color-background-1);
|
||||
border: 1px solid var(--color-foreground-6);
|
||||
border-radius: var(--border-radius-round);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
position: relative;
|
||||
transition: transform ease-in-out 0.2s;
|
||||
width: 2.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.toggle label::after {
|
||||
background-color: var(--color-foreground-6);
|
||||
border-radius: var(--border-radius-round);
|
||||
content: " ";
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
transition: transform ease-in-out 0.2s;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.toggle input[type="checkbox"]:checked ~ label {
|
||||
background-color: var(--color-background-1);
|
||||
border-color: var(--color-foreground-6);
|
||||
}
|
||||
|
||||
.toggle input[type="checkbox"]:checked ~ label::after {
|
||||
background-color: var(--color-foreground-6);
|
||||
transform: translateX(15px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="toggle">
|
||||
<input type="checkbox" bind:checked on:change {id} />
|
||||
<label for={id} />
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" context="module">
|
||||
export interface ToggleButtonOption<T> {
|
||||
title?: string;
|
||||
count?: number;
|
||||
value: T;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" strictEvents>
|
||||
type T = $$Generic;
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { capitalize } from "@app/lib/utils";
|
||||
|
||||
export let options: ToggleButtonOption<T>[];
|
||||
export let active: T;
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: T }>();
|
||||
|
||||
function onSelect(option: ToggleButtonOption<T>) {
|
||||
if (option.count !== 0) {
|
||||
dispatch("select", option.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
user-select: none;
|
||||
}
|
||||
button {
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--color-foreground-6);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-tiny);
|
||||
height: var(--button-tiny-height);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
min-width: 0;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
button:hover,
|
||||
button.active {
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-foreground-1);
|
||||
}
|
||||
button[disabled],
|
||||
button[disabled]:hover {
|
||||
cursor: not-allowed;
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper">
|
||||
{#each options as option}
|
||||
<button
|
||||
class="state-toggle"
|
||||
on:click={() => onSelect(option)}
|
||||
disabled={option.count === 0}
|
||||
class:active={active === option.value}>
|
||||
{#if option.count !== undefined}
|
||||
{option.count}
|
||||
{/if}
|
||||
{option.title ?? capitalize(`${option.value}`)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
Reference in New Issue
Block a user