diff --git a/package-lock.json b/package-lock.json
index 4b8b090ac..67286d53a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@tsconfig/svelte": "^5.0.4",
"@types/node": "^20.9.0",
"baconjs": "^3.0.19",
+ "bs58": "^6.0.0",
"eslint": "^9.9.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.43.0",
@@ -31,7 +32,8 @@
"tslib": "^2.7.0",
"typescript": "^5.2.2",
"typescript-eslint": "^8.4.0",
- "vite": "^5.4.2"
+ "vite": "^5.4.2",
+ "zod": "^3.23.8"
},
"engines": {
"node": "20.9.0"
@@ -1447,6 +1449,12 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/base-x": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz",
+ "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==",
+ "dev": true
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1481,6 +1489,15 @@
"node": ">=8"
}
},
+ "node_modules/bs58": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
+ "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
+ "dev": true,
+ "dependencies": {
+ "base-x": "^5.0.0"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3203,6 +3220,15 @@
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
"dev": true
+ },
+ "node_modules/zod": {
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index 6dbefcfc6..4ec291d5b 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@tsconfig/svelte": "^5.0.4",
"@types/node": "^20.9.0",
"baconjs": "^3.0.19",
+ "bs58": "^6.0.0",
"eslint": "^9.9.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.43.0",
@@ -39,6 +40,7 @@
"tslib": "^2.7.0",
"typescript": "^5.2.2",
"typescript-eslint": "^8.4.0",
- "vite": "^5.4.2"
+ "vite": "^5.4.2",
+ "zod": "^3.23.8"
}
}
diff --git a/public/index.css b/public/index.css
index 72f71d322..da888352f 100644
--- a/public/index.css
+++ b/public/index.css
@@ -15,3 +15,16 @@ html {
height: 100%;
width: 100%;
}
+
+.global-oid {
+ color: var(--color-foreground-emphasized);
+ font-size: var(--font-size-small);
+ font-family: var(--font-family-monospace);
+ font-weight: var(--font-weight-regular);
+}
+
+.global-flex {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
diff --git a/src/components/Border.svelte b/src/components/Border.svelte
index b89b736d5..23d586e5a 100644
--- a/src/components/Border.svelte
+++ b/src/components/Border.svelte
@@ -2,6 +2,8 @@
export let variant: "primary" | "secondary" | "ghost";
export let stylePadding: string | undefined = undefined;
export let styleHeight: string | undefined = undefined;
+ export let styleMinHeight: string | undefined = undefined;
+ export let styleWidth: string | undefined = undefined;
$: style = `--button-color-1: var(--color-fill-${variant});`;
@@ -121,16 +123,27 @@
"p3-1 p3-2 p3-3 p3-4 p3-5"
"p4-1 p4-2 p4-3 p4-4 p4-5"
"p5-1 p5-2 p5-3 p5-4 p5-5";
+ overflow: hidden;
+ }
+
+ .container:hover .p2-3,
+ .container:hover .p3-2,
+ .container:hover .p3-3,
+ .container:hover .p3-4,
+ .container:hover .p4-3 {
+ background-color: var(--color-background-float);
}
@@ -146,7 +159,7 @@
-
+
diff --git a/src/components/Fill.svelte b/src/components/Fill.svelte
index 82707fc07..4b7ef9ae9 100644
--- a/src/components/Fill.svelte
+++ b/src/components/Fill.svelte
@@ -1,7 +1,14 @@
+
+
+
+
+
+
+
+
Radicle Heartwood Protocol & Stack
+
+
{formatRepositoryId(repo.rid)}
+
+
+
+
diff --git a/src/lib/api/author.ts b/src/lib/api/author.ts
new file mode 100644
index 000000000..e5c06bc17
--- /dev/null
+++ b/src/lib/api/author.ts
@@ -0,0 +1,8 @@
+import { object, string, z } from "zod";
+
+export type Author = z.infer
;
+
+export const authorSchema = object({
+ id: string(),
+ alias: string().optional(),
+});
diff --git a/src/lib/api/fixtures/heartwood-repo.json b/src/lib/api/fixtures/heartwood-repo.json
new file mode 100644
index 000000000..6a0e9561a
--- /dev/null
+++ b/src/lib/api/fixtures/heartwood-repo.json
@@ -0,0 +1,36 @@
+{
+ "rid": "rad:z3trNYnLWS11cJWC6BbxDs5niGo82",
+ "payloads": {
+ "xyz.radicle.project": {
+ "data": {
+ "name": "rips",
+ "description": "Radicle Improvement Proposals (RIPs)",
+ "defaultBranch": "master"
+ },
+ "meta": {
+ "head": "329dee9a4b65169ea3889a7da239892b705d0d68",
+ "patches": {
+ "open": 0,
+ "draft": 2,
+ "archived": 0,
+ "merged": 0
+ },
+ "issues": {
+ "open": 1,
+ "closed": 0
+ }
+ }
+ }
+ },
+ "delegates": [
+ {
+ "id": "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
+ "alias": "cloudhead"
+ }
+ ],
+ "threshold": 1,
+ "visibility": {
+ "type": "private"
+ },
+ "seeding": 29
+}
diff --git a/src/lib/api/repo.ts b/src/lib/api/repo.ts
new file mode 100644
index 000000000..5671305c5
--- /dev/null
+++ b/src/lib/api/repo.ts
@@ -0,0 +1,48 @@
+import {
+ array,
+ literal,
+ number,
+ object,
+ optional,
+ string,
+ union,
+ z,
+} from "zod";
+
+import { authorSchema } from "./author";
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const repoSchema = object({
+ rid: string(),
+ payloads: object({
+ "xyz.radicle.project": object({
+ data: object({
+ name: string(),
+ description: string(),
+ defaultBranch: string(),
+ }),
+ meta: object({
+ head: string(),
+ patches: object({
+ open: number(),
+ draft: number(),
+ archived: number(),
+ merged: number(),
+ }),
+ issues: object({
+ open: number(),
+ closed: number(),
+ }),
+ }),
+ }),
+ }),
+ delegates: array(authorSchema),
+ threshold: number(),
+ visibility: union([
+ object({ type: literal("public") }),
+ object({ type: literal("private"), allow: optional(array(string())) }),
+ ]),
+ seeding: number(),
+});
+
+export type Repo = z.infer;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 311b0426d..e24d800a7 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,3 +1,75 @@
+import bs58 from "bs58";
+
export const unreachable = (value: never): never => {
throw new Error(`Unreachable code: ${value}`);
};
+
+export function formatRepositoryId(id: string): string {
+ const parsedId = parseRepositoryId(id);
+
+ if (parsedId) {
+ return `${parsedId.prefix}${truncateId(parsedId.pubkey)}`;
+ }
+
+ return id;
+}
+
+export function parseRepositoryId(
+ rid: string,
+): { prefix: string; pubkey: string } | undefined {
+ const match = /^(rad:)?(z[a-zA-Z0-9]+)$/.exec(rid);
+ if (match) {
+ const hex = bs58.decode(match[2].substring(1));
+ if (hex.byteLength !== 20) {
+ return undefined;
+ }
+
+ return { prefix: match[1] || "rad:", pubkey: match[2] };
+ }
+
+ return undefined;
+}
+
+export function truncateId(pubkey: string): string {
+ return `${pubkey.substring(0, 6)}…${pubkey.slice(-6)}`;
+}
+
+export const formatTimestamp = (
+ timestamp: number,
+ current = new Date().getTime(),
+): string => {
+ const units: Record = {
+ year: 24 * 60 * 60 * 1000 * 365,
+ month: (24 * 60 * 60 * 1000 * 365) / 12,
+ day: 24 * 60 * 60 * 1000,
+ hour: 60 * 60 * 1000,
+ minute: 60 * 1000,
+ second: 1000,
+ };
+
+ // Multiplying timestamp with 1000 to convert from seconds to milliseconds
+ timestamp = timestamp * 1000;
+ const rtf = new Intl.RelativeTimeFormat("en", {
+ numeric: "auto",
+ style: "long",
+ });
+ const elapsed = current - timestamp;
+
+ if (elapsed > units["year"]) {
+ return "more than a year ago";
+ } else if (elapsed < 0) {
+ return "now"; // If elapsed is a negative number we are dealing with an item from the future, and we return "now"
+ }
+
+ for (const u in units) {
+ if (elapsed > units[u] || u === "second") {
+ // We convert the division result to a negative number to get "XX [unit] ago"
+ return rtf.format(
+ Math.round(elapsed / units[u]) * -1,
+ u as Intl.RelativeTimeFormatUnit,
+ );
+ }
+ }
+
+ return new Date(timestamp).toUTCString();
+};
diff --git a/src/views/Home.svelte b/src/views/Home.svelte
index 6815ad2f8..b87abe35b 100644
--- a/src/views/Home.svelte
+++ b/src/views/Home.svelte
@@ -1,8 +1,51 @@
-
+
-👉 Design System
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 👉 Design System
+
+