Add breadcrumbs

This commit is contained in:
Rūdolfs Ošiņš
2024-09-13 13:54:39 +02:00
parent 33e0b52e23
commit 5725cec33f
9 changed files with 320 additions and 47 deletions
+45
View File
@@ -0,0 +1,45 @@
<script lang="ts">
import { createIcon } from "@app/lib/blockies";
export let nodeId: string;
function createContainer(source: string) {
const seed = source.toLowerCase();
const avatar = createIcon({
seed,
size: 8,
scale: 16,
});
return avatar.toDataURL();
}
</script>
<style>
.avatar {
display: block;
width: inherit;
object-fit: cover;
background-size: cover;
background-repeat: no-repeat;
width: 1rem;
height: 1rem;
clip-path: polygon(
0 2px,
2px 2px,
2px 0,
calc(100% - 2px) 0,
calc(100% - 2px) 2px,
100% 2px,
100% calc(100% - 2px),
calc(100% - 2px) calc(100% - 2px),
calc(100% - 2px) calc(100% - 2px),
calc(100% - 2px) 100%,
2px 100%,
2px calc(100% - 2px),
0 calc(100% - 2px)
);
background-color: red;
}
</style>
<img title={nodeId} src={createContainer(nodeId)} class="avatar" alt="avatar" />
+55 -38
View File
@@ -11,7 +11,7 @@
.header {
padding: 0 0.5rem;
gap: 0.25rem;
height: 3rem;
height: 5rem;
}
.wrapper {
width: 100%;
@@ -27,7 +27,7 @@
top: 0;
left: 0.5rem;
right: 0.5rem;
height: 3rem;
height: 5rem;
z-index: -1;
background-color: var(--color-background-float);
@@ -53,44 +53,61 @@
</style>
<div class="header global-flex">
<div class="wrapper global-flex">
<div class="wrapper-left global-flex" style:gap="0">
<div class="global-flex" style:gap="0">
<NakedButton
variant="ghost"
onclick={() => {
window.history.back();
}}>
<Icon name="arrow-left" />
</NakedButton>
<NakedButton
variant="ghost"
onclick={() => {
window.history.forward();
}}>
<Icon name="arrow-right" />
</NakedButton>
<div
class="global-flex"
style:flex-direction="column"
style:width="100%"
style:align-items="flex-start">
<div class="wrapper global-flex">
<div class="wrapper-left global-flex" style:gap="0">
<div class="global-flex" style:gap="0">
<NakedButton
variant="ghost"
onclick={() => {
window.history.back();
}}>
<Icon name="arrow-left" />
</NakedButton>
<NakedButton
variant="ghost"
onclick={() => {
window.history.forward();
}}>
<Icon name="arrow-right" />
</NakedButton>
</div>
<slot name="icon-left" />
</div>
<slot name="center" />
<div class="global-flex" style:gap="0.5rem">
<OutlineButton variant="ghost">
<Icon name="offline" />
Offline
</OutlineButton>
<Popover popoverPositionRight="0" popoverPositionTop="3rem">
<NakedButton
variant="ghost"
slot="toggle"
let:toggle
onclick={toggle}>
<Icon name="more-vertical" />
</NakedButton>
<Border variant="ghost" slot="popover" stylePadding="0.5rem 1rem">
<div style="display: flex; gap: 2rem; align-items: center;">
Theme <ThemeSwitch />
</div>
</Border>
</Popover>
</div>
<slot name="icon-left" />
</div>
<slot name="center" />
<div class="global-flex" style:gap="0.5rem">
<OutlineButton variant="ghost">
<Icon name="offline" />
Offline
</OutlineButton>
<Popover popoverPositionRight="0" popoverPositionTop="3rem">
<NakedButton variant="ghost" slot="toggle" let:toggle onclick={toggle}>
<Icon name="more-vertical" />
</NakedButton>
<Border variant="ghost" slot="popover" stylePadding="0.5rem 1rem">
<div style="display: flex; gap: 2rem; align-items: center;">
Theme <ThemeSwitch />
</div>
</Border>
</Popover>
<div
class="global-flex txt-tiny txt-semibold"
style:gap="0.5rem"
style:margin-left="1rem"
style:min-height="1.5rem">
<slot name="breadcrumbs" />
</div>
</div>
<div class="bottom-pixel-corners"></div>
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import { truncateId } from "@app/lib/utils";
import Avatar from "./Avatar.svelte";
export let nodeId: string;
export let alias: string | undefined = undefined;
export let styleFontSize: string | undefined = "var(--font-size-small)";
export let styleFontFamily: string | undefined =
"var(--font-family-monospace)";
</script>
<style>
.avatar-alias {
display: flex;
align-items: center;
gap: 0.375rem;
height: 1rem;
font-weight: var(--font-weight-semibold);
}
.no-alias {
color: var(--color-foreground-dim);
}
</style>
<div
class="avatar-alias"
style:font-size={styleFontSize}
style:font-family={styleFontFamily}>
<Avatar {nodeId} />
{#if alias}
<span class="txt-overflow">
{alias}
</span>
{:else}
<span class="no-alias">
{truncateId(nodeId)}
</span>
{/if}
</div>
+125
View File
@@ -0,0 +1,125 @@
// Copyright (c) 2019, Ethereum Name Service
// The random number is a js implementation of the Xorshift PRNG
const randseed = new Array(4); // Xorshift: [x, y, z, w] 32 bit values
function seedrand(seed: string) {
for (let i = 0; i < randseed.length; i++) {
randseed[i] = 0;
}
for (let i = 0; i < seed.length; i++) {
randseed[i % 4] =
(randseed[i % 4] << 5) - randseed[i % 4] + seed.charCodeAt(i);
}
}
function rand(): number {
// Based on Java's String.hashCode(), expanded to 4 32bit values.
const t = randseed[0] ^ (randseed[0] << 11);
randseed[0] = randseed[1];
randseed[1] = randseed[2];
randseed[2] = randseed[3];
randseed[3] = randseed[3] ^ (randseed[3] >> 19) ^ t ^ (t >> 8);
return (randseed[3] >>> 0) / ((1 << 31) >>> 0);
}
function createColor(): string {
// Saturation is the whole color spectrum.
const h = Math.floor(rand() * 360);
// Saturation goes from 40 to 100, it avoids greyish colors.
const s = rand() * 60 + 40 + "%";
// Lightness can be anything from 0 to 100, but probabilities are a bell curve around 50%.
const l = (rand() + rand() + rand() + rand()) * 25 + "%";
return `hsl(${h}, ${s}, ${l})`;
}
function createImageData(size: number): number[] {
const width = size;
const height = size;
const dataWidth = Math.ceil(width / 2);
const mirrorWidth = width - dataWidth;
const data = [];
for (let y = 0; y < height; y++) {
let row = [];
for (let x = 0; x < dataWidth; x++) {
// this makes foreground and background color to have a 43% (1/2.3) probability
// spot color has 13% chance
row[x] = Math.floor(rand() * 2.3);
}
const r = row.slice(0, mirrorWidth);
r.reverse();
row = row.concat(r);
for (let i = 0; i < row.length; i++) {
data.push(row[i]);
}
}
return data;
}
function createCanvas(
imageData: number[],
color: string,
scale: number,
bgcolor: string,
spotcolor: string,
): HTMLCanvasElement {
const c = document.createElement("canvas");
const width = Math.sqrt(imageData.length);
c.width = c.height = width * scale;
const cc = c.getContext("2d");
if (!cc) throw new Error("Can't get 2D context");
cc.fillStyle = bgcolor;
cc.fillRect(0, 0, c.width, c.height);
cc.fillStyle = color;
for (let i = 0; i < imageData.length; i++) {
const row = Math.floor(i / width);
const col = i % width;
// if data is 2, choose spot color, if 1 choose foreground
cc.fillStyle = imageData[i] === 1 ? color : spotcolor;
// if data is 0, leave the background
if (imageData[i]) {
cc.fillRect(col * scale, row * scale, scale, scale);
}
}
return c;
}
export interface Options {
seed: string;
size: number;
scale: number;
color?: string;
bgcolor?: string;
spotcolor?: string;
}
export function createIcon(opts: Options): HTMLCanvasElement {
opts = opts || {};
const size = opts.size || 8;
const scale = opts.scale || 4;
const seed =
opts.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16);
seedrand(seed);
const color = opts.color || createColor();
const bgcolor = opts.bgcolor || createColor();
const spotcolor = opts.spotcolor || createColor();
const imageData = createImageData(size);
const canvas = createCanvas(imageData, color, scale, bgcolor, spotcolor);
return canvas;
}
+8
View File
@@ -7,6 +7,7 @@
import CopyableId from "@app/components/CopyableId.svelte";
import Header from "@app/components/Header.svelte";
import RepoCard from "@app/components/RepoCard.svelte";
import NodeId from "@app/components/NodeId.svelte";
export let repos: RepoInfo[];
export let config: Config;
@@ -68,6 +69,13 @@
<svelte:fragment slot="center">
<CopyableId id={`did:key:${config.publicKey}`} />
</svelte:fragment>
<svelte:fragment slot="breadcrumbs">
<NodeId
nodeId={config.publicKey}
alias={config.alias}
styleFontFamily="var(--font-family-sans-serif)"
styleFontSize="var(--font-size-tiny)" />
</svelte:fragment>
</Header>
</div>
<div style:padding="1rem">
+18
View File
@@ -1,14 +1,32 @@
<script lang="ts">
import type { Config } from "@bindings/Config";
import type { Issue } from "@bindings/Issue";
import type { RepoInfo } from "@bindings/RepoInfo";
import Icon from "@app/components/Icon.svelte";
import Layout from "./Layout.svelte";
import NodeId from "@app/components/NodeId.svelte";
export let repo: RepoInfo;
export let issues: Issue[];
export let config: Config;
$: project = repo.payloads["xyz.radicle.project"]!;
</script>
<Layout {repo}>
<svelte:fragment slot="breadcrumbs">
<NodeId
nodeId={config.publicKey}
alias={config.alias}
styleFontFamily="var(--font-family-sans-serif)"
styleFontSize="var(--font-size-tiny)" />
<Icon name="chevron-right" />
{project.data.name}
<Icon name="chevron-right" />
Issues
</svelte:fragment>
<pre>
<!-- prettier-ignore -->
{#each issues as issue}
+3 -3
View File
@@ -8,8 +8,6 @@
import NakedButton from "@app/components/NakedButton.svelte";
export let repo: RepoInfo;
$: project = repo.payloads["xyz.radicle.project"]!;
</script>
<style>
@@ -30,9 +28,11 @@
<svelte:fragment slot="center">
<CopyableId id={repo.rid} />
</svelte:fragment>
<svelte:fragment slot="breadcrumbs">
<slot name="breadcrumbs" />
</svelte:fragment>
</Header>
</div>
<div>{project.data.name}</div>
Issues
<Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
+17
View File
@@ -1,14 +1,31 @@
<script lang="ts">
import type { Config } from "@bindings/Config";
import type { Patch } from "@bindings/Patch";
import type { RepoInfo } from "@bindings/RepoInfo";
import Icon from "@app/components/Icon.svelte";
import Layout from "./Layout.svelte";
import NodeId from "@app/components/NodeId.svelte";
export let repo: RepoInfo;
export let patches: Patch[];
export let config: Config;
$: project = repo.payloads["xyz.radicle.project"]!;
</script>
<Layout {repo}>
<svelte:fragment slot="breadcrumbs">
<NodeId
nodeId={config.publicKey}
alias={config.alias}
styleFontFamily="var(--font-family-sans-serif)"
styleFontSize="var(--font-size-tiny)" />
<Icon name="chevron-right" />
{project.data.name}
<Icon name="chevron-right" />
Patches
</svelte:fragment>
<pre>
<!-- prettier-ignore -->
{#each patches as patch}
+9 -6
View File
@@ -1,6 +1,7 @@
import type { RepoInfo } from "@bindings/RepoInfo";
import type { Patch } from "@bindings/Patch";
import type { Config } from "@bindings/Config";
import type { Issue } from "@bindings/Issue";
import type { Patch } from "@bindings/Patch";
import type { RepoInfo } from "@bindings/RepoInfo";
import { invoke } from "@tauri-apps/api/core";
import { unreachable } from "@app/lib/utils";
@@ -13,7 +14,7 @@ export interface RepoIssuesRoute {
export interface LoadedRepoIssuesRoute {
resource: "repo.issues";
params: { repo: RepoInfo; issues: Issue[] };
params: { repo: RepoInfo; config: Config; issues: Issue[] };
}
export interface RepoPatchesRoute {
@@ -24,7 +25,7 @@ export interface RepoPatchesRoute {
export interface LoadedRepoPatchesRoute {
resource: "repo.patches";
params: { repo: RepoInfo; patches: Patch[] };
params: { repo: RepoInfo; config: Config; patches: Patch[] };
}
export type RepoRoute = RepoIssuesRoute | RepoPatchesRoute;
@@ -34,24 +35,26 @@ export async function loadPatches(route: RepoRoute): Promise<LoadedRepoRoute> {
const repo: RepoInfo = await invoke("repo_by_id", {
rid: route.rid,
});
const config: Config = await invoke("config");
const patches: Patch[] = await invoke("list_patches", {
rid: route.rid,
status: route.status,
});
return { resource: "repo.patches", params: { repo, patches } };
return { resource: "repo.patches", params: { repo, config, patches } };
}
export async function loadIssues(route: RepoRoute): Promise<LoadedRepoRoute> {
const repo: RepoInfo = await invoke("repo_by_id", {
rid: route.rid,
});
const config: Config = await invoke("config");
const issues: Issue[] = await invoke("list_issues", {
rid: route.rid,
status: route.status,
});
return { resource: "repo.issues", params: { repo, issues } };
return { resource: "repo.issues", params: { repo, config, issues } };
}
export function repoRouteToPath(route: RepoRoute): string {