on main help sign in
add basic pals and groups integration + zuse 417
commit7e2bd151ee2dfb1d4d46d7b236a10f40d9773b42 authorJohn Hyde <johnhydefour@gmail.com> dateTue 6 Dec 2022, 11:09 (4 years ago) parent39c101c
20 files changed+900−174
desk/desk.docket-0+2−2 desk/sys.kelvin+1−1 ui/package-lock.json+49−3 ui/package.json+1 ui/src/components/ExtensionWindow.svelte+25 ui/src/components/ShipListing.svelte+12−4 ui/src/components/ShipView.svelte+21−119 ui/src/components/Sigil.svelte+5−1 ui/src/components/groups/GroupsShipViewExtension.svelte+50 ui/src/components/pals/PalStatusIndicator.svelte+191 ui/src/components/pals/PalTagger.svelte+40 ui/src/components/pals/PalsShipViewExtension.svelte+33 ui/src/lib/api.ts+2 ui/src/lib/pals.ts+91 ui/src/lib/ship.ts+114 ui/src/lib/sigil.ts+1−1 ui/src/lib/utils.ts+10 ui/src/stores/store.ts+18−3 ui/src/styles/tailwind-output.css+205−40 ui/src/types/store.ts+29
desk/desk.docket-0+2−2
@@ -2,9 +2,9 @@
22 info+'Your guide to the Urbit constellations'
33 color+0x1f.243c
44 image+'https://raw.githubusercontent.com/johnhyde/astrolabe/main/ui/src/assets/astrolabe-tile.svg'
5− glob-http+['https://s3.us-east-2.amazonaws.com/warkgnall.com/urbit/astrolabe/globs/glob-0v2.9eih9.a77pv.83cmp.gc70u.ev4q7.glob' 0v2.9eih9.a77pv.83cmp.gc70u.ev4q7]
5+ glob-http+['https://s3.us-east-2.amazonaws.com/warkgnall.com/urbit/astrolabe/globs/glob-0v4.fpk0a.pgb9r.m1e4o.ptne3.pnthp.glob' 0v4.fpk0a.pgb9r.m1e4o.ptne3.pnthp]
66 base+'astrolabe'
7− version+[0 3 5]
7+ version+[0 3 6]
88 website+'https://github.com/johnhyde/astrolabe'
99 license+'MIT'
1010==
desk/sys.kelvin+1−1
@@ -1 +1 @@
1−[%zuse 418]
1+[%zuse 417]
ui/package-lock.json+49−3
@@ -19,6 +19,7 @@
1919 "lodash": "^4.17.21",
2020 "sigma": "^2.3.1",
2121 "svelte-copy-to-clipboard": "^0.2.5",
22+ "svelte-markdown": "^0.2.3",
2223 "tippy.js": "^6.3.7",
2324 "urbit-ob": "^5.0.1"
2425 },
@@ -323,6 +324,11 @@
323324 "integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==",
324325 "dev": true
325326 },
327+ "node_modules/@types/marked": {
328+ "version": "4.0.7",
329+ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
330+ "integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw=="
331+ },
326332 "node_modules/@types/node": {
327333 "version": "17.0.8",
328334 "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
@@ -1776,6 +1782,17 @@
17761782 "sourcemap-codec": "^1.4.4"
17771783 }
17781784 },
1785+ "node_modules/marked": {
1786+ "version": "4.2.3",
1787+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.3.tgz",
1788+ "integrity": "sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw==",
1789+ "bin": {
1790+ "marked": "bin/marked.js"
1791+ },
1792+ "engines": {
1793+ "node": ">= 12"
1794+ }
1795+ },
17791796 "node_modules/merge2": {
17801797 "version": "1.4.1",
17811798 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2609,7 +2626,6 @@
26092626 "version": "3.48.0",
26102627 "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.48.0.tgz",
26112628 "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==",
2612− "dev": true,
26132629 "engines": {
26142630 "node": ">= 8"
26152631 }
@@ -2651,6 +2667,18 @@
26512667 "svelte": ">=3.19.0"
26522668 }
26532669 },
2670+ "node_modules/svelte-markdown": {
2671+ "version": "0.2.3",
2672+ "resolved": "https://registry.npmjs.org/svelte-markdown/-/svelte-markdown-0.2.3.tgz",
2673+ "integrity": "sha512-2h680NzTXnAD0CXhxe3GeHl6W+ayG4iKQRl+BIDRw+R0mUE0OiNxP1Vt8Rn+aWevB/LBiBIPCAwvL+0BkG057A==",
2674+ "dependencies": {
2675+ "@types/marked": "^4.0.1",
2676+ "marked": "^4.0.10"
2677+ },
2678+ "peerDependencies": {
2679+ "svelte": "^3.0.0"
2680+ }
2681+ },
26542682 "node_modules/svelte-preprocess": {
26552683 "version": "4.10.1",
26562684 "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.1.tgz",
@@ -3256,6 +3284,11 @@
32563284 "integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==",
32573285 "dev": true
32583286 },
3287+ "@types/marked": {
3288+ "version": "4.0.7",
3289+ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.7.tgz",
3290+ "integrity": "sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw=="
3291+ },
32593292 "@types/node": {
32603293 "version": "17.0.8",
32613294 "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
@@ -4308,6 +4341,11 @@
43084341 "sourcemap-codec": "^1.4.4"
43094342 }
43104343 },
4344+ "marked": {
4345+ "version": "4.2.3",
4346+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.3.tgz",
4347+ "integrity": "sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw=="
4348+ },
43114349 "merge2": {
43124350 "version": "1.4.1",
43134351 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -4883,8 +4921,7 @@
48834921 "svelte": {
48844922 "version": "3.48.0",
48854923 "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.48.0.tgz",
4886− "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==",
4887− "dev": true
4924+ "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ=="
48884925 },
48894926 "svelte-check": {
48904927 "version": "2.2.11",
@@ -4915,6 +4952,15 @@
49154952 "dev": true,
49164953 "requires": {}
49174954 },
4955+ "svelte-markdown": {
4956+ "version": "0.2.3",
4957+ "resolved": "https://registry.npmjs.org/svelte-markdown/-/svelte-markdown-0.2.3.tgz",
4958+ "integrity": "sha512-2h680NzTXnAD0CXhxe3GeHl6W+ayG4iKQRl+BIDRw+R0mUE0OiNxP1Vt8Rn+aWevB/LBiBIPCAwvL+0BkG057A==",
4959+ "requires": {
4960+ "@types/marked": "^4.0.1",
4961+ "marked": "^4.0.10"
4962+ }
4963+ },
49184964 "svelte-preprocess": {
49194965 "version": "4.10.1",
49204966 "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.1.tgz",
ui/package.json+1
@@ -54,6 +54,7 @@
5454 "lodash": "^4.17.21",
5555 "sigma": "^2.3.1",
5656 "svelte-copy-to-clipboard": "^0.2.5",
57+ "svelte-markdown": "^0.2.3",
5758 "tippy.js": "^6.3.7",
5859 "urbit-ob": "^5.0.1"
5960 }
ui/src/components/ExtensionWindow.sveltenew file+25
@@ -0,0 +1,25 @@
1+<script lang="ts">
2+ export let name: string;
3+ export let open = true;
4+ export let halfWidth = false;
5+ $: containerClasses = open ? ('w-full ' + (halfWidth ? 'md:w-1/2' : '')) : 'w-min justify-self-start';
6+ $: boxClasses = open ? 'border-gray-500' : 'border-gray-400 bg-gray-400';
7+ $: buttonClasses = open ? 'text-gray-600' : 'text-white';
8+</script>
9+
10+<div class="p-1 {containerClasses}">
11+ <div class="border rounded-md {boxClasses}" on:click={() => { if (!open) open = true; }}>
12+ <div class="flex flex-wrap">
13+ <button class="px-2 text-sm {buttonClasses}" on:click|stopPropagation={() => open = !open}>
14+ {name}
15+ </button>
16+ {#if open}
17+ <slot name="top-bar"/>
18+ {/if}
19+ </div>
20+ {#if open}
21+ <slot/>
22+ {/if}
23+ </div>
24+</div>
25+
ui/src/components/ShipListing.svelte+12−4
@@ -1,9 +1,12 @@
11<script lang="ts">
22
33 import Sigil from "./Sigil.svelte";
4+ import PalStatusIndicator from '@/pals/PalStatusIndicator.svelte';
45 import { push } from 'svelte-spa-router'
56 import { normalizeId } from 'lib/id';
67 import { linkToShip as generateLinkToShip } from 'lib/link';
8+ import { getPalStatus } from 'lib/pals';
9+ import store from 'stores/store';
710 export let ship: any;
811 export let linkToShip: boolean = false;
912 export let differentiateContacts = false;
@@ -24,13 +27,18 @@
2427on:click={handleClick}
2528on:click
2629>
27− <div class="flex flex-wrap p-4 items-center">
28− <Sigil {patp} size={48} useNew />
30+ <div class="flex p-4 items-center gap-x-4">
31+ <Sigil {patp} size={48} useNew noShrink />
2932 <!-- {#if ship.avatar}
3033 <img src={ship.avatar} class="h-12 w-12 mr-4" />
3134 {/if} -->
32− <p class="ml-4 break-words">
33− {patp}<br/>{ship.nickname ? `(${ship.nickname})` : ''}
35+ <p class="break-words grow">
36+ <span class="align-middle mr-1">
37+ {patp}
38+ </span>
39+ <PalStatusIndicator status={getPalStatus($store.pals, patp)} class="inline" />
40+ <br/>
41+ {ship.nickname ? `(${ship.nickname})` : ''}
3442 </p>
3543 </div>
3644</div>
ui/src/components/ShipView.svelte+21−119
@@ -1,8 +1,12 @@
11<script lang="ts">
2− import { clan, sein, patp2dec } from 'urbit-ob';
2+
3+
4+
5+ import { clan, patp2dec } from 'urbit-ob';
36
47 import { cite, normalizeId } from 'lib/id';
58 import { getPoint, getSpawnedPoints } from 'lib/api';
9+ import { generateParentChain, processPointInfo } from 'lib/ship';
610 import Sigil from "./Sigil.svelte";
711 import GoldBadge from './GoldBadge.svelte';
812 import TooltipAndDocLink from "./TooltipAndDocLink.svelte";
@@ -12,73 +16,26 @@
1216 import CollapsibleContent from './CollapsibleContent.svelte';
1317 import ShipChain from './ShipChain.svelte';
1418 import ShipListings from './ShipListings.svelte';
15− import LoadingSpinner from './LoadingSpinner.svelte';
19+ import LoadingSpinner from './LoadingSpinner.svelte';;
20+ import GroupsShipViewExtension from './groups/GroupsShipViewExtension.svelte';
21+ import PalsShipViewExtension from './pals/PalsShipViewExtension.svelte';
1622
1723 export let patp: string;
1824
1925 let parentChain: any[];
20− let sponsorChain: any[];
2126 let rawPointInfoPromise: any;
2227 let rawPointInfo: any = null;
2328 let pointInfo: any = {};
2429 let rawSpawnedPointsPromise: any;
2530 let rawSpawnedPoints: any = null;
2631 let spawnedPoints: string[] = [];
27− const spawnStatusOptions = {
28− unspawned: {
29− text: 'unspawned',
30− tooltip: 'This point has not yet been spawned by its parent',
31− },
32− locked: {
33− text: 'locked',
34− tooltip: 'This point is currently locked in a Linear Release Contract',
35− },
36− spawned: {
37− text: 'spawned',
38− tooltip: `This point has been spawned`,
39− },
40− spawnedNoKeys: {
41− text: 'spawned (no keys set)',
42− tooltip: 'This point has been spawned, but no keys have been set',
43− },
44− }
45− const layerOptions = {
46− l1: {
47− text: 'on layer 1',
48− tooltip: 'This point is secured as an NFT on the Ethereum blockchain',
49− },
50− l2: {
51− text: 'on layer 2',
52− tooltip: `This point is secured by Urbit's "Layer 2" solution`,
53− },
54− spawn: {
55− text: 'spawns on layer 2',
56− tooltip: 'This point lives on layer 1, but it spawns new points on layer 2',
57− },
58− }
5932
6033 $: shipClass = clan(patp);
6134 $: azPoint = ['galaxy', 'star', 'planet'].includes(shipClass);
6235 $: canSpawnPoints = ['galaxy', 'star'].includes(shipClass);
6336 $: spawnableClassPlural = !canSpawnPoints ? '' : (shipClass === 'galaxy' ? 'Stars' : 'Planets');
6437 $: {
65− let parents = [patp];
66− let galaxyFound = false;
67− let count = 10;
68− while (!galaxyFound && count > 0) {
69− count--;
70− if (count == 0) {
71− throw new Error("You've got an infinite loop in building the parent chain");
72− }
73− let firstItem = parents[0];
74− if (clan(firstItem) == 'galaxy') {
75− galaxyFound = true;
76− } else {
77− parents.unshift(sein(firstItem))
78− }
79− }
80− parents.pop();
81− parentChain = parents;
38+ parentChain = generateParentChain(patp);
8239 }
8340 $: {
8441 rawPointInfo = null;
@@ -110,71 +67,9 @@
11067 }
11168 $: {
11269 pointInfo = {};
113− let spawnStatus: any,
114− spawnedUnlocked: boolean = false,
115− layer: any,
116− life: number,
117− rift: number,
118− spawnedCount: number;
119− let keysSet = false;
120− if (!azPoint) {
121−
122− } else if (rawPointInfo) {
123− if (rawPointInfo.point) {
124− const { point } = rawPointInfo;
125− const {
126− 'probable-dominion': dominion,
127− npoint
128− } = point;
129− layer = layerOptions[dominion];
130− spawnedCount = point['spa-count'];
131− sponsorChain = point['sponsor-chain'].reverse();
132− if (npoint) {
133− life = npoint.net.keys.life;
134− rift = npoint.net.rift;
135− if (life > 0) {
136− keysSet = true;
137− }
138−
139− pointInfo.proxies = npoint.own;
140− if (pointInfo.proxies.owner.address === '0x86cd9cd0992f04231751e3761de45cecea5d1801') {
141− spawnStatus = spawnStatusOptions.locked;
142− } else if (keysSet) {
143− spawnStatus = spawnStatusOptions.spawned;
144− spawnedUnlocked = true;
145− } else {
146− spawnStatus = spawnStatusOptions.spawnedNoKeys;
147− spawnedUnlocked = true;
148− }
149− // const { sponsor } = npoint.net;
150− // if (sponsor.has) {
151− // pointInfo.sponsor = normalizeId(sponsor.who);
152− // }
153− } else {
154− // no info found
155− spawnStatus = spawnStatusOptions.unspawned;
156− life = 0;
157− rift = 0;
158− }
159− } else {
160− // no info found
161− spawnStatus = spawnStatusOptions.unspawned;
162− life = 0;
163− rift = 0;
164− }
165− } else {
166− // request pending
70+ if (azPoint) {
71+ pointInfo = processPointInfo(rawPointInfo);
16772 }
168− pointInfo = {
169− ...pointInfo,
170− layer,
171− spawnStatus,
172− spawnedUnlocked,
173− keysSet,
174− life,
175− rift,
176− spawnedCount,
177− };
17873 }
17974</script>
18075
@@ -192,6 +87,9 @@
19287 {cite(patp)}
19388 </TooltipAndDocLink>
19489 </h2>
90+ <!-- {#if (nickname)}
91+ <h3 class="text-lg text-center text-gray-800">{nickname}</h3>
92+ {/if} -->
19593 {#if (!azPoint)}
19694 <h3 class="text-lg text-center text-gray-700">{patp}</h3>
19795 {/if}
@@ -243,10 +141,10 @@
243141 {:then}
244142 {#if shipClass !== 'galaxy'}
245143 <p>
246− {#if sponsorChain && sponsorChain.length > 0}
144+ {#if pointInfo.sponsorChain && pointInfo.sponsorChain.length > 0}
247145 <p>
248− {sponsorChain.length === 1 ? 'Sponsor:' : 'Sponsor Chain:'}
249− <ShipChain shipChain={sponsorChain}></ShipChain>
146+ {pointInfo.sponsorChain.length === 1 ? 'Sponsor:' : 'Sponsor Chain:'}
147+ <ShipChain shipChain={pointInfo.sponsorChain}></ShipChain>
250148 </p>
251149 {:else}
252150 Unsponsored
@@ -328,6 +226,10 @@
328226 </div>
329227 </div>
330228 </div>
229+ <div class="flex flex-wrap shrink min-h-0 h-fit -m-1 mt-2 justify-evenly justify-items-center">
230+ <GroupsShipViewExtension {patp} />
231+ <PalsShipViewExtension {patp} />
232+ </div>
331233 {#if canSpawnPoints}
332234 <div class="mt-5 p-3 border-t">
333235 {#await rawPointInfoPromise then}
ui/src/components/Sigil.svelte+5−1
@@ -12,9 +12,13 @@
1212 export let fgColor: string = 'white';
1313 export let bgColor: string = 'black';
1414 export let altBgColor: string = undefined;
15+ export let noShrink: boolean = false;
1516 let svgString: any;
1617 let imgSrc: any;
1718
19+ $: noShrinkClasses = noShrink ? 'shrink-0' : '';
20+ $: noShrinkWidth = noShrink ? size+'px' : 'initial';
21+
1822 $: shipClass = clan(patp);
1923 $: displaySigil = ['galaxy', 'star', 'planet'].includes(shipClass);
2024 $: {
@@ -52,7 +56,7 @@
5256 }
5357</script>
5458
55−<div class="grow" style:max-width="{size}px">
59+<div class="grow {noShrinkClasses}" style:max-width="{size}px" style:width={noShrinkWidth}>
5660 {#if displaySigil}
5761 {@html svgString}
5862 {:else}
ui/src/components/groups/GroupsShipViewExtension.sveltenew file+50
@@ -0,0 +1,50 @@
1+<script lang="ts">
2+ import SvelteMarkdown from 'svelte-markdown';
3+ import { uxToHex } from 'lib/utils';
4+ import { normalizeId } from 'lib/id';
5+ import store from 'stores/store';
6+ import Sigil from "@/Sigil.svelte";
7+ import ExtensionWindow from '@/ExtensionWindow.svelte';
8+
9+ export let patp: string;
10+ $: contact = $store.contacts[normalizeId(patp)];
11+ $: color = contact && uxToHex(contact.color);
12+ $: somethingToShow = contact && (contact.nickname || contact.cover || contact.avatar || contact.bio);
13+</script>
14+
15+{#if somethingToShow}
16+ <ExtensionWindow name="Groups">
17+ <svelte:fragment slot="top-bar">
18+ {#if contact.nickname}
19+ <div class="grow ml-1 my-1 flex justify-center align-center">
20+ {contact.nickname}
21+ </div>
22+ {/if}
23+ </svelte:fragment>
24+ <div class="flex flex-wrap py-2">
25+ {#if contact.cover}
26+ <img src={contact.cover} alt="profile cover image"
27+ class="m-1 -mb-16 w-full h-48 border border-black rounded-lg object-cover min-w-0"
28+ />
29+ {/if}
30+ <div class="flex flex-wrap md:flex-nowrap justify-center">
31+ <div class="mx-8 max-w-[128px] min-w-[128px] h-fit grow overflow-hidden rounded-lg">
32+ {#if contact.avatar}
33+ <img src={contact.avatar} alt="custom avatar"
34+ class="aspect-square object-cover max-w-[128px] min-w-0"
35+ />
36+ {:else}
37+ <Sigil patp={patp} size={128} bgColor={color} useNew />
38+ {/if}
39+ </div>
40+ <div class="text-sm p-3 bg-white rounded-tl-lg rounded-br-lg whitespace-pre-line">
41+ <SvelteMarkdown source={contact.bio} />
42+ </div>
43+ </div>
44+ <!-- <p class="whitespace-pre-line">
45+ {contact.bio}
46+ </p> -->
47+ </div>
48+ </ExtensionWindow>
49+{/if}
50+
ui/src/components/pals/PalStatusIndicator.sveltenew file+191
@@ -0,0 +1,191 @@
1+<script lang="ts">
2+ import type { PalStatus } from 'lib/pals';
3+ import tooltip from 'actions/tooltip';
4+ export let status: PalStatus = 'unconnected';
5+ export let acked: boolean = true;
6+ let className: string = '';
7+ export { className as class };
8+
9+ let statusDescription = '', viewBox = '', width = '25';
10+ $: {
11+ switch (status) {
12+ case 'mutual':
13+ statusDescription = 'you are pals'
14+ viewBox = '12 12 100 100';
15+ width = '25';
16+ break;
17+ case 'outgoing':
18+ statusDescription = 'you added them in pals'
19+ viewBox = '0 12 68 100';
20+ width = '17';
21+ if (!acked) {
22+ statusDescription += ' (pending)'
23+ }
24+ break;
25+ case 'incoming':
26+ statusDescription = 'they added you in pals'
27+ viewBox = '24 12 76 100';
28+ width = '19';
29+ break;
30+ }
31+ }
32+</script>
33+{#if status !== 'unconnected'}
34+ <svg width={width} height="25" {viewBox} fill="none" xmlns="http://www.w3.org/2000/svg"
35+ use:tooltip={statusDescription}
36+ class={className}
37+ >
38+
39+ {#if status == 'mutual'}
40+ <path d="M108 50
41+ C108 61.9348 103.259 73.3807 94.8198 81.8198
42+ C86.3807 90.2589 74.9347 95 63 95
43+ C51.0653 95 39.6193 90.2589 31.1802 81.8198
44+ C22.7411 73.3807 18 61.9347 18 50" stroke="black" stroke-width="10"></path><path d="M87.5114 31.5994
45+ C87.5114 28.4354 84.9226 25.8466 81.7585 25.8466
46+ C78.5945 25.8466 76.0057 28.4354 76.0057 31.5994
47+ C76.0057 34.7635 78.5945 37.3523 81.7585 37.3523
48+ C84.9226 37.3523 87.5114 34.7635 87.5114 31.5994
49+ Z
50+ M51.0767 31.5994
51+ C51.0767 28.4354 48.4879 25.8466 45.3239 25.8466
52+ C42.1598 25.8466 39.571 28.4354 39.571 31.5994
53+ C39.571 34.7635 42.1598 37.3523 45.3239 37.3523
54+ C48.4879 37.3523 51.0767 34.7635 51.0767 31.5994
55+ Z" fill="black"></path>
56+ {:else if status == 'outgoing' }
57+ {#if acked}
58+ <path d="M50 95
59+ C44.0905 95 38.2389 93.836 32.7792 91.5746
60+ C27.3196 89.3131 22.3588 85.9984 18.1802 81.8198
61+ C14.0016 77.6412 10.6869 72.6804 8.42542 67.2208
62+ C6.16396 61.7611 5 55.9095 5 50" stroke="black" stroke-width="10"></path><path d="M30.5546 65.9099
63+ L28.7868 67.6777
64+ L32.3223 71.2132
65+ L34.0901 69.4454
66+ L30.5546 65.9099
67+ Z
68+ M67.6777 32.3223
69+ L39.7938 39.7938
70+ L60.2062 60.2062
71+ L67.6777 32.3223
72+ Z
73+ M34.0901 69.4454
74+ L53.5355 50
75+ L50 46.4645
76+ L30.5546 65.9099
77+ L34.0901 69.4454
78+ Z" fill="black"></path>
79+ {:else}
80+ <path d="M50 95
81+ C44.0905 95 38.2389 93.836 32.7792 91.5746
82+ C27.3196 89.3131 22.3588 85.9984 18.1802 81.8198
83+ C14.0016 77.6412 10.6869 72.6804 8.42542 67.2208
84+ C6.16396 61.7611 5 55.9095 5 50" stroke="black" stroke-width="10"></path><path d="M31.4384 66.7938
85+ L30.5546 67.6777
86+ L32.3223 69.4454
87+ L33.2062 68.5616
88+ L31.4384 66.7938
89+ Z
90+ M67.6777 32.3223
91+ L53.7357 36.0581
92+ L63.9419 46.2643
93+ L67.6777 32.3223
94+ Z
95+ M33.2062 68.5616
96+ L34.974 66.7938
97+ L33.2062 65.026
98+ L31.4384 66.7938
99+ L33.2062 68.5616
100+ Z
101+ M38.5095 63.2583
102+ L42.045 59.7227
103+ L40.2773 57.955
104+ L36.7417 61.4905
105+ L38.5095 63.2583
106+ Z
107+ M45.5806 56.1872
108+ L49.1161 52.6517
109+ L47.3483 50.8839
110+ L43.8128 54.4194
111+ L45.5806 56.1872
112+ Z
113+ M52.6516 49.1161
114+ L56.1872 45.5806
115+ L54.4194 43.8128
116+ L50.8839 47.3484
117+ L52.6516 49.1161
118+ Z
119+ M59.7227 42.0451
120+ L63.2583 38.5095
121+ L61.4905 36.7418
122+ L57.9549 40.2773
123+ L59.7227 42.0451
124+ Z
125+ M30.5546 65.9099
126+ L28.7868 67.6777
127+ L32.3223 71.2132
128+ L34.0901 69.4454
129+ L30.5546 65.9099
130+ Z
131+ M67.6777 32.3223
132+ L39.7938 39.7938
133+ L60.2062 60.2062
134+ L67.6777 32.3223
135+ Z
136+ M34.0901 69.4454
137+ L35.8579 67.6777
138+ L32.3223 64.1421
139+ L30.5546 65.9099
140+ L34.0901 69.4454
141+ Z
142+ M39.3934 64.1421
143+ L42.9289 60.6066
144+ L39.3934 57.0711
145+ L35.8579 60.6066
146+ L39.3934 64.1421
147+ Z
148+ M46.4645 57.0711
149+ L50 53.5355
150+ L46.4645 50
151+ L42.9289 53.5355
152+ L46.4645 57.0711
153+ Z
154+ M53.5355 50
155+ L57.0711 46.4645
156+ L53.5355 42.9289
157+ L50 46.4645
158+ L53.5355 50
159+ Z
160+ M60.6066 42.9289
161+ L64.1421 39.3934
162+ L60.6066 35.8579
163+ L57.0711 39.3934
164+ L60.6066 42.9289
165+ Z" fill="black"></path>
166+ {/if}
167+ {:else if status == 'incoming' }
168+ <path d="M95 50
169+ C95 55.9095 93.836 61.7611 91.5746 67.2208
170+ C89.3131 72.6804 85.9984 77.6412 81.8198 81.8198
171+ C77.6412 85.9984 72.6804 89.3131 67.2208 91.5746
172+ C61.7611 93.836 55.9095 95 50 95" stroke="black" stroke-width="10"></path><path d="M67.2383 68.7383
173+ L59.7669 40.8545
174+ L39.3545 61.2669
175+ L67.2383 68.7383
176+ Z
177+ M31.5294 29.4939
178+ L29.7617 27.7262
179+ L26.2261 31.2617
180+ L27.9939 33.0295
181+ L31.5294 29.4939
182+ Z
183+ M53.0962 51.0607
184+ L31.5294 29.4939
185+ L27.9939 33.0295
186+ L49.5607 54.5962
187+ L53.0962 51.0607
188+ Z" fill="black"></path>
189+ {/if}
190+ </svg>
191+{/if}
ui/src/components/pals/PalTagger.sveltenew file+40
@@ -0,0 +1,40 @@
1+<script lang="ts">
2+ import { getPal, tagPal, untagPal } from 'lib/pals';
3+ import store from 'stores/store';
4+
5+ export let patp: string;
6+
7+ $: pal = getPal($store.pals, patp);
8+
9+ let newTagText = '';
10+ $: newTags = newTagText.split(',').map(t => t.trim());
11+ $: sanitizedNewTags = newTags.map(t => t.replaceAll(/[^-_.~a-z0-9]+/g, '_'));
12+ $: newTagsAreValid = newTags.every(tagIsValid);
13+ $: validationClasses = newTagsAreValid ? '' : 'border-red-500 bg-red-100'
14+
15+ function tagIsValid(tag: string): boolean {
16+ return /^[-_.~a-z0-9]*$/.test(tag);
17+ }
18+
19+ async function onTagSubmit({ key }) {
20+ if (newTagText && key == 'Enter') {
21+ await tagPal(patp, sanitizedNewTags);
22+ newTagText = '';
23+ }
24+ }
25+</script>
26+
27+<div class="flex flex-wrap gap-1 text-sm justify-center">
28+ {#each pal.tags as tag}
29+ <div class="flex bg-gray-200 rounded divide-gray-500 divide-x items-center">
30+ <span class="px-1">
31+ {tag}
32+ </span>
33+ <button class="px-1 text-gray-500" on:click={() => untagPal(patp, tag)}>
34+ X
35+ </button>
36+ </div>
37+ {/each}
38+</div>
39+<input type="text" class="px-1 text-sm border border-gray-400 rounded {validationClasses}"
40+ on:keydown={onTagSubmit} bind:value={newTagText} placeholder="add_tags, like_this" />
ui/src/components/pals/PalsShipViewExtension.sveltenew file+33
@@ -0,0 +1,33 @@
1+<script lang="ts">
2+ import { getPal, addPal, removePal } from 'lib/pals';
3+ import store from 'stores/store';
4+ import ExtensionWindow from '@/ExtensionWindow.svelte';
5+ import PalStatusIndicator from './PalStatusIndicator.svelte';
6+ import PalTagger from './PalTagger.svelte';
7+
8+ export let patp: string;
9+
10+ $: pal = getPal($store.pals, patp);
11+ $: palAdded = pal.status == 'outgoing' || pal.status == 'mutual';
12+ $: [buttonText, buttonFunc] = palAdded ? ['Remove Pal', removePal] : ['Add Pal', addPal];
13+</script>
14+
15+{#if $store.palsInstalled}
16+ <ExtensionWindow name="%pals" halfWidth>
17+ <svelte:fragment slot="top-bar">
18+ <div class="grow ml-1 my-2 flex justify-center align-center">
19+ <PalStatusIndicator status={pal.status} acked={pal.ack} />
20+ <button class="bg-gray-200 ml-3 px-2 rounded" on:click={() => buttonFunc(patp)}>
21+ {buttonText}
22+ </button>
23+ </div>
24+ </svelte:fragment>
25+
26+ {#if palAdded}
27+ <div class="flex flex-wrap justify-center gap-3 my-3 conteents">
28+ <PalTagger {patp}></PalTagger>
29+ </div>
30+ {/if}
31+ </ExtensionWindow>
32+{/if}
33+
ui/src/lib/api.ts+2
@@ -10,6 +10,8 @@ const api: UrbitApi = new UrbitApi('', '', window.desk);
1010api.ship = window.ship;
1111// api.connect();
1212
13+export { api };
14+
1315export function scry<T = any>(path: string): Promise<T> {
1416 return api.scry<T>({ app: 'astrolabe', path });
1517}
ui/src/lib/pals.tsnew file+91
@@ -0,0 +1,91 @@
1+import type { Patp } from '@urbit/api';
2+// import uniq from 'lodash/without';
3+// import without from 'lodash/without';
4+import type { Pals, PalList, PalNames, Pal, PalInfo, PalStatus } from 'types/store';
5+import store from 'stores/store';
6+import { api } from 'lib/api';
7+import { normalizeId, normalizeIdAndDesig } from 'lib/id';
8+
9+export function getPals() {
10+ return api.scry<any>({ app: 'pals', path: '/json' }).then(processPals);
11+}
12+
13+function palCommand(cmd: string, ship: string, tags: string[] = []) {
14+ return {
15+ [cmd]: {
16+ ship: normalizeIdAndDesig(ship),
17+ in: tags,
18+ },
19+ };
20+}
21+
22+export async function sendPalCommand(cmd: string, ship: string, tags: string[] = []) {
23+ let result = await api.poke({
24+ app: 'pals',
25+ mark: 'pals-command',
26+ json: palCommand(cmd, ship, tags),
27+ });
28+ store.refreshPals();
29+ return result;
30+}
31+
32+export function addPal(patp: string, tags: string[] = []) {
33+ return sendPalCommand('meet', patp, tags);
34+}
35+
36+export function removePal(patp: string) {
37+ return sendPalCommand('part', patp, []);
38+}
39+
40+export function tagPal(patp: string, tag: (string | string[])) {
41+ if (typeof tag == 'string') tag = [tag];
42+ return addPal(patp, tag);
43+}
44+
45+export function untagPal(patp: string, tag: (string | string[])) {
46+ if (typeof tag == 'string') tag = [tag];
47+ return sendPalCommand('part', patp, tag);
48+}
49+
50+export function processPals(rawPals: any): Pals {
51+ let incoming: PalNames = {};
52+ let outgoing: PalList = {};
53+ let mutuals: PalList = {};
54+
55+ Object.entries(rawPals.incoming).forEach(([patp, bool]) => {
56+ incoming[normalizeId(patp)] = bool as boolean;
57+ });
58+ (Object.entries(rawPals.outgoing) as [string, any][]).forEach(([patp, { lists, ack }]) => {
59+ patp = normalizeId(patp);
60+ outgoing[patp] = {
61+ tags: lists as string[],
62+ ack: ack as boolean,
63+ };
64+ if (incoming[patp]) {
65+ mutuals[patp] = outgoing[patp];
66+ }
67+ });
68+
69+ return {
70+ incoming,
71+ outgoing,
72+ mutuals,
73+ };
74+}
75+
76+export function getPalStatus(pals: Pals, patp: Patp): PalStatus {
77+ if (pals.mutuals[patp]) return 'mutual';
78+ if (pals.outgoing[patp]) return 'outgoing';
79+ if (pals.incoming[patp]) return 'incoming';
80+ return 'unconnected';
81+}
82+
83+export function getPal(pals: Pals, patp: Patp): PalInfo {
84+ return {
85+ patp,
86+ status: getPalStatus(pals, patp),
87+ tags: [],
88+ ack: null,
89+ ...pals.outgoing[patp],
90+ };
91+}
ui/src/lib/ship.tsnew file+114
@@ -0,0 +1,114 @@
1+import { clan, sein } from 'urbit-ob';
2+
3+export const spawnStatusOptions = {
4+ unspawned: {
5+ text: 'unspawned',
6+ tooltip: 'This point has not yet been spawned by its parent',
7+ },
8+ locked: {
9+ text: 'locked',
10+ tooltip: 'This point is currently locked in a Linear Release Contract',
11+ },
12+ spawned: {
13+ text: 'spawned',
14+ tooltip: `This point has been spawned`,
15+ },
16+ spawnedNoKeys: {
17+ text: 'spawned (no keys set)',
18+ tooltip: 'This point has been spawned, but no keys have been set',
19+ },
20+};
21+export const layerOptions = {
22+ l1: {
23+ text: 'on layer 1',
24+ tooltip: 'This point is secured as an NFT on the Ethereum blockchain',
25+ },
26+ l2: {
27+ text: 'on layer 2',
28+ tooltip: `This point is secured by Urbit's "Layer 2" solution`,
29+ },
30+ spawn: {
31+ text: 'spawns on layer 2',
32+ tooltip: 'This point lives on layer 1, but it spawns new points on layer 2',
33+ },
34+};
35+
36+export function generateParentChain(patp: string): any[] {
37+ let parents = [patp];
38+ let galaxyFound = false;
39+ let count = 10;
40+ while (!galaxyFound && count > 0) {
41+ count--;
42+ if (count == 0) {
43+ throw new Error("You've got an infinite loop in building the parent chain");
44+ }
45+ let firstItem = parents[0];
46+ if (clan(firstItem) == 'galaxy') {
47+ galaxyFound = true;
48+ } else {
49+ parents.unshift(sein(firstItem))
50+ }
51+ }
52+ parents.pop();
53+ return parents;
54+}
55+
56+export function processPointInfo(rawPointInfo: any): any {
57+ let pointInfo: any = {};
58+ let spawnStatus: any,
59+ spawnedUnlocked: boolean = false,
60+ layer: any,
61+ life: number,
62+ rift: number,
63+ spawnedCount: number,
64+ sponsorChain: any[];
65+ let keysSet = false;
66+ if (!rawPointInfo) {
67+ // request pending
68+ } else {
69+ // defaults in case no info found
70+ spawnStatus = spawnStatusOptions.unspawned;
71+ life = 0;
72+ rift = 0;
73+
74+ if (rawPointInfo.point) {
75+ const { point } = rawPointInfo;
76+ const {
77+ 'probable-dominion': dominion,
78+ npoint
79+ } = point;
80+ layer = layerOptions[dominion];
81+ spawnedCount = point['spa-count'];
82+ sponsorChain = point['sponsor-chain'].reverse();
83+
84+ if (npoint) {
85+ life = npoint.net.keys.life;
86+ rift = npoint.net.rift;
87+ if (life > 0) {
88+ keysSet = true;
89+ }
90+ pointInfo.proxies = npoint.own;
91+ if (pointInfo.proxies.owner.address === '0x86cd9cd0992f04231751e3761de45cecea5d1801') {
92+ spawnStatus = spawnStatusOptions.locked;
93+ } else if (keysSet) {
94+ spawnStatus = spawnStatusOptions.spawned;
95+ spawnedUnlocked = true;
96+ } else {
97+ spawnStatus = spawnStatusOptions.spawnedNoKeys;
98+ spawnedUnlocked = true;
99+ }
100+ }
101+ }
102+ }
103+ return {
104+ ...pointInfo,
105+ layer,
106+ spawnStatus,
107+ spawnedUnlocked,
108+ keysSet,
109+ life,
110+ rift,
111+ spawnedCount,
112+ sponsorChain,
113+ };
114+}
ui/src/lib/sigil.ts+1−1
@@ -123,7 +123,7 @@ export class SigilQuery {
123123 }
124124 });
125125 } catch (e) {
126− console.error("Couldn't load in sigil query text", e);
126+ console.log("Couldn't load in sigil query text:", e.message);
127127 this.clan = 'planet';
128128 symbols = [];
129129 }
ui/src/lib/utils.ts+10
@@ -45,3 +45,13 @@ export function toggleStoreKey(store, key) {
4545export function capitalize(string: string): string {
4646 return string.charAt(0).toUpperCase() + string.slice(1);
4747}
48+
49+export function uxToHex(ux: string) {
50+ let value = ux;
51+ if (ux.length > 2 && ux.substring(0, 2) === '0x') {
52+ value = ux.substring(2)
53+ }
54+
55+ value = value.replace('.', '').padStart(6, '0');
56+ return '#' + value;
57+}
ui/src/stores/store.ts+18−3
@@ -3,12 +3,19 @@ import { writable } from 'svelte/store';
33import type { Rolodex } from '@urbit/api';
44import { subscribeToContacts, getPeers } from 'lib/api';
55import type { StoreState } from 'types/store';
6−import { normalizeId } from 'lib/id';
7−import { setStoreKey } from 'lib/utils';
6+import { normalizeId } from 'lib/id';
7+import { setStoreKey } from 'lib/utils';
8+import { getPals, getPalStatus, getPal } from 'lib/pals';
89
910const initStore: StoreState = {
1011 contacts: {},
1112 peers: [],
13+ pals: {
14+ incoming: {},
15+ outgoing: {},
16+ mutuals: {},
17+ },
18+ palsInstalled: false,
1219 patpQuery: null,
1320 sigilQuery: null,
1421 searchMode: 'patp',
@@ -48,4 +55,12 @@ getPeers().then(({ points }) => {
4855 setStoreKey(store, 'peers', points.map(normalizeId));
4956});
5057
51−export default { subscribe, set, searchPatp, searchSigil, reset };
58+function refreshPals() {
59+ getPals().then((pals) => {
60+ setStoreKey(store, 'pals', pals);
61+ setStoreKey(store, 'palsInstalled', true);
62+ });
63+}
64+refreshPals();
65+
66+export default { subscribe, set, searchPatp, searchSigil, refreshPals, reset };
ui/src/styles/tailwind-output.css+205−40
@@ -571,6 +571,9 @@ Ensure the default browser behavior of the `hidden` attribute.
571571.col-span-2 {
572572 grid-column: span 2 / span 2;
573573}
574+.-m-1 {
575+ margin: -0.25rem;
576+}
574577.m-4 {
575578 margin: 1rem;
576579}
@@ -633,6 +636,10 @@ Ensure the default browser behavior of the `hidden` attribute.
633636 margin-top: auto;
634637 margin-bottom: auto;
635638}
639+.-mx-1 {
640+ margin-left: -0.25rem;
641+ margin-right: -0.25rem;
642+}
636643.mt-2 {
637644 margin-top: 0.5rem;
638645}
@@ -645,8 +652,8 @@ Ensure the default browser behavior of the `hidden` attribute.
645652.mr-4 {
646653 margin-right: 1rem;
647654}
648−.ml-4 {
649− margin-left: 1rem;
655+.mr-1 {
656+ margin-right: 0.25rem;
650657}
651658.mt-4 {
652659 margin-top: 1rem;
@@ -657,6 +664,9 @@ Ensure the default browser behavior of the `hidden` attribute.
657664.ml-8 {
658665 margin-left: 2rem;
659666}
667+.ml-4 {
668+ margin-left: 1rem;
669+}
660670.-mt-2 {
661671 margin-top: -0.5rem;
662672}
@@ -699,8 +709,20 @@ Ensure the default browser behavior of the `hidden` attribute.
699709.mb-1 {
700710 margin-bottom: 0.25rem;
701711}
702−.mr-1 {
703− margin-right: 0.25rem;
712+.mr-2 {
713+ margin-right: 0.5rem;
714+}
715+.mr-0 {
716+ margin-right: 0px;
717+}
718+.-mb-24 {
719+ margin-bottom: -6rem;
720+}
721+.-mb-20 {
722+ margin-bottom: -5rem;
723+}
724+.-mb-16 {
725+ margin-bottom: -4rem;
704726}
705727.-mt-0\.5 {
706728 margin-top: -0.125rem;
@@ -741,6 +763,11 @@ Ensure the default browser behavior of the `hidden` attribute.
741763.h-12 {
742764 height: 3rem;
743765}
766+.h-fit {
767+ height: -webkit-fit-content;
768+ height: -moz-fit-content;
769+ height: fit-content;
770+}
744771.h-3 {
745772 height: 0.75rem;
746773}
@@ -789,21 +816,40 @@ Ensure the default browser behavior of the `hidden` attribute.
789816.h-auto {
790817 height: auto;
791818}
819+.h-0 {
820+ height: 0px;
821+}
822+.h-min {
823+ height: -webkit-min-content;
824+ height: -moz-min-content;
825+ height: min-content;
826+}
827+.h-40 {
828+ height: 10rem;
829+}
830+.h-60 {
831+ height: 15rem;
832+}
833+.min-h-0 {
834+ min-height: 0px;
835+}
792836.min-h-\[20px\] {
793837 min-height: 20px;
794838}
795839.min-h-full {
796840 min-height: 100%;
797841}
798−.min-h-0 {
799− min-height: 0px;
800−}
801842.min-h-\[14rem\] {
802843 min-height: 14rem;
803844}
804845.w-full {
805846 width: 100%;
806847}
848+.w-min {
849+ width: -webkit-min-content;
850+ width: -moz-min-content;
851+ width: min-content;
852+}
807853.w-5 {
808854 width: 1.25rem;
809855}
@@ -858,12 +904,25 @@ Ensure the default browser behavior of the `hidden` attribute.
858904.w-40 {
859905 width: 10rem;
860906}
907+.w-fit {
908+ width: -webkit-fit-content;
909+ width: -moz-fit-content;
910+ width: fit-content;
911+}
912+.w-max {
913+ width: -webkit-max-content;
914+ width: -moz-max-content;
915+ width: max-content;
916+}
861917.w-\[48px\] {
862918 width: 48px;
863919}
864920.min-w-0 {
865921 min-width: 0px;
866922}
923+.min-w-\[128px\] {
924+ min-width: 128px;
925+}
867926.max-w-md {
868927 max-width: 28rem;
869928}
@@ -882,6 +941,15 @@ Ensure the default browser behavior of the `hidden` attribute.
882941.max-w-2xl {
883942 max-width: 42rem;
884943}
944+.max-w-xl {
945+ max-width: 36rem;
946+}
947+.max-w-lg {
948+ max-width: 32rem;
949+}
950+.max-w-\[128px\] {
951+ max-width: 128px;
952+}
885953.max-w-\[10rem\] {
886954 max-width: 10rem;
887955}
@@ -900,6 +968,9 @@ Ensure the default browser behavior of the `hidden` attribute.
900968.shrink {
901969 flex-shrink: 1;
902970}
971+.shrink-0 {
972+ flex-shrink: 0;
973+}
903974.flex-grow {
904975 flex-grow: 1;
905976}
@@ -1010,6 +1081,9 @@ Ensure the default browser behavior of the `hidden` attribute.
10101081.grid-flow-col {
10111082 grid-auto-flow: column;
10121083}
1084+.grid-flow-row-dense {
1085+ grid-auto-flow: row dense;
1086+}
10131087.grid-flow-col-dense {
10141088 grid-auto-flow: column dense;
10151089}
@@ -1038,6 +1112,9 @@ Ensure the default browser behavior of the `hidden` attribute.
10381112.grid-rows-none {
10391113 grid-template-rows: none;
10401114}
1115+.grid-rows-3 {
1116+ grid-template-rows: repeat(3, minmax(0, 1fr));
1117+}
10411118.flex-col {
10421119 flex-direction: column;
10431120}
@@ -1065,9 +1142,18 @@ Ensure the default browser behavior of the `hidden` attribute.
10651142.justify-center {
10661143 justify-content: center;
10671144}
1145+.justify-between {
1146+ justify-content: space-between;
1147+}
1148+.justify-evenly {
1149+ justify-content: space-evenly;
1150+}
10681151.justify-items-center {
10691152 justify-items: center;
10701153}
1154+.justify-items-stretch {
1155+ justify-items: stretch;
1156+}
10711157.gap-5 {
10721158 gap: 1.25rem;
10731159}
@@ -1083,6 +1169,13 @@ Ensure the default browser behavior of the `hidden` attribute.
10831169.gap-2 {
10841170 gap: 0.5rem;
10851171}
1172+.gap-3 {
1173+ gap: 0.75rem;
1174+}
1175+.gap-x-4 {
1176+ -moz-column-gap: 1rem;
1177+ column-gap: 1rem;
1178+}
10861179.gap-x-1 {
10871180 -moz-column-gap: 0.25rem;
10881181 column-gap: 0.25rem;
@@ -1095,10 +1188,6 @@ Ensure the default browser behavior of the `hidden` attribute.
10951188 -moz-column-gap: 0.75rem;
10961189 column-gap: 0.75rem;
10971190}
1098−.gap-x-4 {
1099− -moz-column-gap: 1rem;
1100− column-gap: 1rem;
1101−}
11021191.space-y-4 > :not([hidden]) ~ :not([hidden]) {
11031192 --tw-space-y-reverse: 0;
11041193 margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
@@ -1138,6 +1227,38 @@ Ensure the default browser behavior of the `hidden` attribute.
11381227 --tw-divide-opacity: 1;
11391228 border-color: rgb(252 211 77 / var(--tw-divide-opacity));
11401229}
1230+.divide-black > :not([hidden]) ~ :not([hidden]) {
1231+ --tw-divide-opacity: 1;
1232+ border-color: rgb(0 0 0 / var(--tw-divide-opacity));
1233+}
1234+.divide-gray-800 > :not([hidden]) ~ :not([hidden]) {
1235+ --tw-divide-opacity: 1;
1236+ border-color: rgb(31 41 55 / var(--tw-divide-opacity));
1237+}
1238+.divide-gray-600 > :not([hidden]) ~ :not([hidden]) {
1239+ --tw-divide-opacity: 1;
1240+ border-color: rgb(75 85 99 / var(--tw-divide-opacity));
1241+}
1242+.divide-red-600 > :not([hidden]) ~ :not([hidden]) {
1243+ --tw-divide-opacity: 1;
1244+ border-color: rgb(220 38 38 / var(--tw-divide-opacity));
1245+}
1246+.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
1247+ --tw-divide-opacity: 1;
1248+ border-color: rgb(229 231 235 / var(--tw-divide-opacity));
1249+}
1250+.divide-gray-400 > :not([hidden]) ~ :not([hidden]) {
1251+ --tw-divide-opacity: 1;
1252+ border-color: rgb(156 163 175 / var(--tw-divide-opacity));
1253+}
1254+.divide-gray-300 > :not([hidden]) ~ :not([hidden]) {
1255+ --tw-divide-opacity: 1;
1256+ border-color: rgb(209 213 219 / var(--tw-divide-opacity));
1257+}
1258+.divide-gray-500 > :not([hidden]) ~ :not([hidden]) {
1259+ --tw-divide-opacity: 1;
1260+ border-color: rgb(107 114 128 / var(--tw-divide-opacity));
1261+}
11411262.place-self-center {
11421263 place-self: center;
11431264}
@@ -1165,6 +1286,13 @@ Ensure the default browser behavior of the `hidden` attribute.
11651286.overflow-y-auto {
11661287 overflow-y: auto;
11671288}
1289+.whitespace-pre-line {
1290+ white-space: pre-line;
1291+}
1292+.break-normal {
1293+ overflow-wrap: normal;
1294+ word-break: normal;
1295+}
11681296.break-words {
11691297 overflow-wrap: break-word;
11701298}
@@ -1209,6 +1337,12 @@ Ensure the default browser behavior of the `hidden` attribute.
12091337.rounded-bl-none {
12101338 border-bottom-left-radius: 0px;
12111339}
1340+.rounded-tl-lg {
1341+ border-top-left-radius: 0.5rem;
1342+}
1343+.rounded-br-lg {
1344+ border-bottom-right-radius: 0.5rem;
1345+}
12121346.border {
12131347 border-width: 1px;
12141348}
@@ -1242,6 +1376,17 @@ Ensure the default browser behavior of the `hidden` attribute.
12421376.border-l {
12431377 border-left-width: 1px;
12441378}
1379+.border-none {
1380+ border-style: none;
1381+}
1382+.border-gray-500 {
1383+ --tw-border-opacity: 1;
1384+ border-color: rgb(107 114 128 / var(--tw-border-opacity));
1385+}
1386+.border-gray-400 {
1387+ --tw-border-opacity: 1;
1388+ border-color: rgb(156 163 175 / var(--tw-border-opacity));
1389+}
12451390.border-gold {
12461391 --tw-border-opacity: 1;
12471392 border-color: rgb(252 211 77 / var(--tw-border-opacity));
@@ -1262,10 +1407,6 @@ Ensure the default browser behavior of the `hidden` attribute.
12621407 --tw-border-opacity: 1;
12631408 border-color: rgb(0 0 0 / var(--tw-border-opacity));
12641409}
1265−.border-gray-400 {
1266− --tw-border-opacity: 1;
1267− border-color: rgb(156 163 175 / var(--tw-border-opacity));
1268−}
12691410.border-red-500 {
12701411 --tw-border-opacity: 1;
12711412 border-color: rgb(239 68 68 / var(--tw-border-opacity));
@@ -1286,14 +1427,18 @@ Ensure the default browser behavior of the `hidden` attribute.
12861427 --tw-border-opacity: 1;
12871428 border-color: rgb(31 41 55 / var(--tw-border-opacity));
12881429}
1289−.border-gray-500 {
1430+.border-gray-600 {
12901431 --tw-border-opacity: 1;
1291− border-color: rgb(107 114 128 / var(--tw-border-opacity));
1432+ border-color: rgb(75 85 99 / var(--tw-border-opacity));
12921433}
12931434.bg-navy {
12941435 --tw-bg-opacity: 1;
12951436 background-color: rgb(31 36 60 / var(--tw-bg-opacity));
12961437}
1438+.bg-gray-400 {
1439+ --tw-bg-opacity: 1;
1440+ background-color: rgb(156 163 175 / var(--tw-bg-opacity));
1441+}
12971442.bg-gold {
12981443 --tw-bg-opacity: 1;
12991444 background-color: rgb(252 211 77 / var(--tw-bg-opacity));
@@ -1322,6 +1467,10 @@ Ensure the default browser behavior of the `hidden` attribute.
13221467 --tw-bg-opacity: 1;
13231468 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
13241469}
1470+.bg-gray-200 {
1471+ --tw-bg-opacity: 1;
1472+ background-color: rgb(229 231 235 / var(--tw-bg-opacity));
1473+}
13251474.bg-black {
13261475 --tw-bg-opacity: 1;
13271476 background-color: rgb(0 0 0 / var(--tw-bg-opacity));
@@ -1334,10 +1483,6 @@ Ensure the default browser behavior of the `hidden` attribute.
13341483 --tw-bg-opacity: 1;
13351484 background-color: rgb(253 224 71 / var(--tw-bg-opacity));
13361485}
1337−.bg-gray-400 {
1338− --tw-bg-opacity: 1;
1339− background-color: rgb(156 163 175 / var(--tw-bg-opacity));
1340−}
13411486.bg-orange-300 {
13421487 --tw-bg-opacity: 1;
13431488 background-color: rgb(253 186 116 / var(--tw-bg-opacity));
@@ -1362,10 +1507,6 @@ Ensure the default browser behavior of the `hidden` attribute.
13621507 --tw-bg-opacity: 1;
13631508 background-color: rgb(253 230 138 / var(--tw-bg-opacity));
13641509}
1365−.bg-gray-200 {
1366− --tw-bg-opacity: 1;
1367− background-color: rgb(229 231 235 / var(--tw-bg-opacity));
1368−}
13691510.bg-gold-t8 {
13701511 --tw-bg-opacity: 1;
13711512 background-color: rgb(254 246 219 / var(--tw-bg-opacity));
@@ -1414,9 +1555,24 @@ Ensure the default browser behavior of the `hidden` attribute.
14141555 --tw-bg-opacity: 1;
14151556 background-color: rgb(107 114 128 / var(--tw-bg-opacity));
14161557}
1558+.bg-red-200 {
1559+ --tw-bg-opacity: 1;
1560+ background-color: rgb(254 202 202 / var(--tw-bg-opacity));
1561+}
1562+.bg-red-100 {
1563+ --tw-bg-opacity: 1;
1564+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
1565+}
14171566.fill-transparent {
14181567 fill: transparent;
14191568}
1569+.object-cover {
1570+ -o-object-fit: cover;
1571+ object-fit: cover;
1572+}
1573+.p-1 {
1574+ padding: 0.25rem;
1575+}
14201576.p-5 {
14211577 padding: 1.25rem;
14221578}
@@ -1435,9 +1591,6 @@ Ensure the default browser behavior of the `hidden` attribute.
14351591.p-6 {
14361592 padding: 1.5rem;
14371593}
1438−.p-1 {
1439− padding: 0.25rem;
1440−}
14411594.p-0 {
14421595 padding: 0px;
14431596}
@@ -1559,6 +1712,10 @@ Ensure the default browser behavior of the `hidden` attribute.
15591712 font-size: 1.875rem;
15601713 line-height: 2.25rem;
15611714}
1715+.text-sm {
1716+ font-size: 0.875rem;
1717+ line-height: 1.25rem;
1718+}
15621719.text-xl {
15631720 font-size: 1.25rem;
15641721 line-height: 1.75rem;
@@ -1571,10 +1728,6 @@ Ensure the default browser behavior of the `hidden` attribute.
15711728 font-size: 1.125rem;
15721729 line-height: 1.75rem;
15731730}
1574−.text-sm {
1575− font-size: 0.875rem;
1576− line-height: 1.25rem;
1577−}
15781731.text-base {
15791732 font-size: 1rem;
15801733 line-height: 1.5rem;
@@ -1598,6 +1751,14 @@ Ensure the default browser behavior of the `hidden` attribute.
15981751 --tw-text-opacity: 1;
15991752 color: rgb(255 255 255 / var(--tw-text-opacity));
16001753}
1754+.text-gray-600 {
1755+ --tw-text-opacity: 1;
1756+ color: rgb(75 85 99 / var(--tw-text-opacity));
1757+}
1758+.text-gray-800 {
1759+ --tw-text-opacity: 1;
1760+ color: rgb(31 41 55 / var(--tw-text-opacity));
1761+}
16011762.text-gray-700 {
16021763 --tw-text-opacity: 1;
16031764 color: rgb(55 65 81 / var(--tw-text-opacity));
@@ -1610,18 +1771,10 @@ Ensure the default browser behavior of the `hidden` attribute.
16101771 --tw-text-opacity: 1;
16111772 color: rgb(22 163 74 / var(--tw-text-opacity));
16121773}
1613−.text-gray-800 {
1614− --tw-text-opacity: 1;
1615− color: rgb(31 41 55 / var(--tw-text-opacity));
1616−}
16171774.text-gray-500 {
16181775 --tw-text-opacity: 1;
16191776 color: rgb(107 114 128 / var(--tw-text-opacity));
16201777}
1621−.text-gray-600 {
1622− --tw-text-opacity: 1;
1623− color: rgb(75 85 99 / var(--tw-text-opacity));
1624−}
16251778.text-green-500 {
16261779 --tw-text-opacity: 1;
16271780 color: rgb(34 197 94 / var(--tw-text-opacity));
@@ -1638,6 +1791,10 @@ Ensure the default browser behavior of the `hidden` attribute.
16381791 --tw-text-opacity: 1;
16391792 color: rgb(252 211 77 / var(--tw-text-opacity));
16401793}
1794+.text-gray-300 {
1795+ --tw-text-opacity: 1;
1796+ color: rgb(209 213 219 / var(--tw-text-opacity));
1797+}
16411798.underline {
16421799 -webkit-text-decoration-line: underline;
16431800 text-decoration-line: underline;
@@ -1816,6 +1973,10 @@ Ensure the default browser behavior of the `hidden` attribute.
18161973 margin-right: auto;
18171974 }
18181975
1976+ .md\:w-1\/2 {
1977+ width: 50%;
1978+ }
1979+
18191980 .md\:min-w-\[256px\] {
18201981 min-width: 256px;
18211982 }
@@ -1832,6 +1993,10 @@ Ensure the default browser behavior of the `hidden` attribute.
18321993 flex-direction: row;
18331994 }
18341995
1996+ .md\:flex-nowrap {
1997+ flex-wrap: nowrap;
1998+ }
1999+
18352000 .md\:items-start {
18362001 align-items: flex-start;
18372002 }
ui/src/types/store.ts+29
@@ -7,6 +7,33 @@ export interface Contacts {
77export interface Docs {
88 [doc: string]: string;
99};
10+
11+export interface PalNames {
12+ [patp: Patp]: boolean;
13+}
14+
15+export interface Pal {
16+ tags: string[];
17+ ack: boolean;
18+}
19+
20+export interface PalList {
21+ [patp: Patp]: Pal;
22+}
23+
24+export interface Pals {
25+ incoming: PalNames;
26+ outgoing: PalList;
27+ mutuals: PalList;
28+}
29+
30+export const PAL_STATUSES = ['mutual', 'outgoing', 'incoming', 'unconnected'] as const;
31+export type PalStatus = typeof PAL_STATUSES[number];
32+export interface PalInfo extends Pal {
33+ patp: Patp;
34+ status: PalStatus;
35+}
36+
1037export interface StoreState {
1138 // Urbit
1239 // groups: {
@@ -14,6 +41,8 @@ export interface StoreState {
1441 // };
1542 contacts: Rolodex;
1643 peers: Patp[];
44+ pals: Pals;
45+ palsInstalled: boolean;
1746 patpQuery: RegExp;
1847 sigilQuery: string[][];
1948 searchMode: ('patp' | 'sigil');