Organise files more consistently

Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
Rūdolfs Ošiņš
2022-12-22 16:46:08 +01:00
parent e86accafae
commit caa83db480
130 changed files with 720 additions and 707 deletions
+132
View File
@@ -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>
+35
View File
@@ -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}
+73
View File
@@ -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">&nbsp;{caption}&nbsp;</span>
<span class="txt-tiny date">
{formatTimestamp(timestamp)}
</span>
</span>
+61
View File
@@ -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 />
+58
View File
@@ -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>
+134
View File
@@ -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>
+49
View File
@@ -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>
+115
View File
@@ -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>
+30
View File
@@ -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}
+64
View File
@@ -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}
@@ -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;
+69
View File
@@ -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>
+51
View File
@@ -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>
+70
View File
@@ -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>
+282
View File
@@ -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">&cross; 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>
+321
View File
@@ -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>
+23
View File
@@ -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>
+58
View File
@@ -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}
+165
View File
@@ -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}
+307
View File
@@ -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>
+18
View File
@@ -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>
+121
View File
@@ -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>
-89
View File
@@ -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)}&nbsp;{project.info.name}
</span>
<span class="urn">
&nbsp;{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>
+23
View File
@@ -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>
+33
View File
@@ -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>
+16
View File
@@ -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>
+51
View File
@@ -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>
+74
View File
@@ -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" />
-121
View File
@@ -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>
+150
View File
@@ -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>
+55
View File
@@ -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>
+71
View File
@@ -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>