on main help sign in
add basic pals and groups integration + zuse 417
7e2bd15John Hyde
desk/desk.docket-0+2−2
@@ -2,9 +2,9 @@
2 info+'Your guide to the Urbit constellations'
2 info+'Your guide to the Urbit constellations'
3 color+0x1f.243c
3 color+0x1f.243c
4 image+'https://raw.githubusercontent.com/johnhyde/astrolabe/main/ui/src/assets/astrolabe-tile.svg'
4 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]
6 base+'astrolabe'
6 base+'astrolabe'
7− version+[0 3 5]
7+ version+[0 3 6]
8 website+'https://github.com/johnhyde/astrolabe'
8 website+'https://github.com/johnhyde/astrolabe'
9 license+'MIT'
9 license+'MIT'
10==
10==
desk/sys.kelvin+1−1
@@ -1 +1 @@
1−[%zuse 418]
1+[%zuse 417]
ui/package-lock.json+49−3
@@ -19,6 +19,7 @@
19 "lodash": "^4.17.21",
19 "lodash": "^4.17.21",
20 "sigma": "^2.3.1",
20 "sigma": "^2.3.1",
21 "svelte-copy-to-clipboard": "^0.2.5",
21 "svelte-copy-to-clipboard": "^0.2.5",
22+ "svelte-markdown": "^0.2.3",
22 "tippy.js": "^6.3.7",
23 "tippy.js": "^6.3.7",
23 "urbit-ob": "^5.0.1"
24 "urbit-ob": "^5.0.1"
24 },
25 },
@@ -323,6 +324,11 @@
323 "integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==",
324 "integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==",
324 "dev": true
325 "dev": true
325 },
326 },
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+ },
326 "node_modules/@types/node": {
332 "node_modules/@types/node": {
327 "version": "17.0.8",
333 "version": "17.0.8",
328 "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
334 "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
@@ -1776,6 +1782,17 @@
1776 "sourcemap-codec": "^1.4.4"
1782 "sourcemap-codec": "^1.4.4"
1777 }
1783 }
1778 },
1784 },
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+ },
1779 "node_modules/merge2": {
1796 "node_modules/merge2": {
1780 "version": "1.4.1",
1797 "version": "1.4.1",
1781 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1798 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2609,7 +2626,6 @@
2609 "version": "3.48.0",
2626 "version": "3.48.0",
2610 "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.48.0.tgz",
2627 "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.48.0.tgz",
2611 "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==",
2628 "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==",
2612− "dev": true,
2613 "engines": {
2629 "engines": {
2614 "node": ">= 8"
2630 "node": ">= 8"
2615 }
2631 }
@@ -2651,6 +2667,18 @@
2651 "svelte": ">=3.19.0"
2667 "svelte": ">=3.19.0"
2652 }
2668 }
2653 },
2669 },
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+ },
2654 "node_modules/svelte-preprocess": {
2682 "node_modules/svelte-preprocess": {
2655 "version": "4.10.1",
2683 "version": "4.10.1",
2656 "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.1.tgz",
2684 "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.1.tgz",
@@ -3256,6 +3284,11 @@
3256 "integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==",
3284 "integrity": "sha512-aqkICXbM1oX5FfgZd2qSSAGdyo/NRxjWCamxoyi3T8iVQnzGge19HhDYzZ6NrVOW7bhcWNSq9XexWFtMzbB24A==",
3257 "dev": true
3285 "dev": true
3258 },
3286 },
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+ },
3259 "@types/node": {
3292 "@types/node": {
3260 "version": "17.0.8",
3293 "version": "17.0.8",
3261 "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
3294 "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
@@ -4308,6 +4341,11 @@
4308 "sourcemap-codec": "^1.4.4"
4341 "sourcemap-codec": "^1.4.4"
4309 }
4342 }
4310 },
4343 },
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+ },
4311 "merge2": {
4349 "merge2": {
4312 "version": "1.4.1",
4350 "version": "1.4.1",
4313 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
4351 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -4883,8 +4921,7 @@
4883 "svelte": {
4921 "svelte": {
4884 "version": "3.48.0",
4922 "version": "3.48.0",
4885 "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.48.0.tgz",
4923 "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.48.0.tgz",
4886− "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==",
4924+ "integrity": "sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ=="
4887− "dev": true
4888 },
4925 },
4889 "svelte-check": {
4926 "svelte-check": {
4890 "version": "2.2.11",
4927 "version": "2.2.11",
@@ -4915,6 +4952,15 @@
4915 "dev": true,
4952 "dev": true,
4916 "requires": {}
4953 "requires": {}
4917 },
4954 },
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+ },
4918 "svelte-preprocess": {
4964 "svelte-preprocess": {
4919 "version": "4.10.1",
4965 "version": "4.10.1",
4920 "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.1.tgz",
4966 "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.1.tgz",
ui/package.json+1
@@ -54,6 +54,7 @@
54 "lodash": "^4.17.21",
54 "lodash": "^4.17.21",
55 "sigma": "^2.3.1",
55 "sigma": "^2.3.1",
56 "svelte-copy-to-clipboard": "^0.2.5",
56 "svelte-copy-to-clipboard": "^0.2.5",
57+ "svelte-markdown": "^0.2.3",
57 "tippy.js": "^6.3.7",
58 "tippy.js": "^6.3.7",
58 "urbit-ob": "^5.0.1"
59 "urbit-ob": "^5.0.1"
59 }
60 }
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 @@
1<script lang="ts">
1<script lang="ts">
2
2
3 import Sigil from "./Sigil.svelte";
3 import Sigil from "./Sigil.svelte";
4+ import PalStatusIndicator from '@/pals/PalStatusIndicator.svelte';
4 import { push } from 'svelte-spa-router'
5 import { push } from 'svelte-spa-router'
5 import { normalizeId } from 'lib/id';
6 import { normalizeId } from 'lib/id';
6 import { linkToShip as generateLinkToShip } from 'lib/link';
7 import { linkToShip as generateLinkToShip } from 'lib/link';
8+ import { getPalStatus } from 'lib/pals';
9+ import store from 'stores/store';
7 export let ship: any;
10 export let ship: any;
8 export let linkToShip: boolean = false;
11 export let linkToShip: boolean = false;
9 export let differentiateContacts = false;
12 export let differentiateContacts = false;
@@ -24,13 +27,18 @@
24on:click={handleClick}
27on:click={handleClick}
25on:click
28on:click
26>
29>
27− <div class="flex flex-wrap p-4 items-center">
30+ <div class="flex p-4 items-center gap-x-4">
28− <Sigil {patp} size={48} useNew />
31+ <Sigil {patp} size={48} useNew noShrink />
29 <!-- {#if ship.avatar}
32 <!-- {#if ship.avatar}
30 <img src={ship.avatar} class="h-12 w-12 mr-4" />
33 <img src={ship.avatar} class="h-12 w-12 mr-4" />
31 {/if} -->
34 {/if} -->
32− <p class="ml-4 break-words">
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/>
33− {patp}<br/>{ship.nickname ? `(${ship.nickname})` : ''}
41+ {ship.nickname ? `(${ship.nickname})` : ''}
34 </p>
42 </p>
35 </div>
43 </div>
36</div>
44</div>
ui/src/components/ShipView.svelte+21−119
@@ -1,8 +1,12 @@
1<script lang="ts">
1<script lang="ts">
2+
3+
4+
2− import { clan, sein, patp2dec } from 'urbit-ob';
5+ import { clan, patp2dec } from 'urbit-ob';
3
6
4 import { cite, normalizeId } from 'lib/id';
7 import { cite, normalizeId } from 'lib/id';
5 import { getPoint, getSpawnedPoints } from 'lib/api';
8 import { getPoint, getSpawnedPoints } from 'lib/api';
9+ import { generateParentChain, processPointInfo } from 'lib/ship';
6 import Sigil from "./Sigil.svelte";
10 import Sigil from "./Sigil.svelte";
7 import GoldBadge from './GoldBadge.svelte';
11 import GoldBadge from './GoldBadge.svelte';
8 import TooltipAndDocLink from "./TooltipAndDocLink.svelte";
12 import TooltipAndDocLink from "./TooltipAndDocLink.svelte";
@@ -12,73 +16,26 @@
12 import CollapsibleContent from './CollapsibleContent.svelte';
16 import CollapsibleContent from './CollapsibleContent.svelte';
13 import ShipChain from './ShipChain.svelte';
17 import ShipChain from './ShipChain.svelte';
14 import ShipListings from './ShipListings.svelte';
18 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';
16
22
17 export let patp: string;
23 export let patp: string;
18
24
19 let parentChain: any[];
25 let parentChain: any[];
20− let sponsorChain: any[];
21 let rawPointInfoPromise: any;
26 let rawPointInfoPromise: any;
22 let rawPointInfo: any = null;
27 let rawPointInfo: any = null;
23 let pointInfo: any = {};
28 let pointInfo: any = {};
24 let rawSpawnedPointsPromise: any;
29 let rawSpawnedPointsPromise: any;
25 let rawSpawnedPoints: any = null;
30 let rawSpawnedPoints: any = null;
26 let spawnedPoints: string[] = [];
31 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− }
59
32
60 $: shipClass = clan(patp);
33 $: shipClass = clan(patp);
61 $: azPoint = ['galaxy', 'star', 'planet'].includes(shipClass);
34 $: azPoint = ['galaxy', 'star', 'planet'].includes(shipClass);
62 $: canSpawnPoints = ['galaxy', 'star'].includes(shipClass);
35 $: canSpawnPoints = ['galaxy', 'star'].includes(shipClass);
63 $: spawnableClassPlural = !canSpawnPoints ? '' : (shipClass === 'galaxy' ? 'Stars' : 'Planets');
36 $: spawnableClassPlural = !canSpawnPoints ? '' : (shipClass === 'galaxy' ? 'Stars' : 'Planets');
64 $: {
37 $: {
65− let parents = [patp];
38+ parentChain = generateParentChain(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;
82 }
39 }
83 $: {
40 $: {
84 rawPointInfo = null;
41 rawPointInfo = null;
@@ -110,71 +67,9 @@
110 }
67 }
111 $: {
68 $: {
112 pointInfo = {};
69 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) {
70+ if (azPoint) {
121−
71+ pointInfo = processPointInfo(rawPointInfo);
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
167 }
72 }
168− pointInfo = {
169− ...pointInfo,
170− layer,
171− spawnStatus,
172− spawnedUnlocked,
173− keysSet,
174− life,
175− rift,
176− spawnedCount,
177− };
178 }
73 }
179</script>
74</script>
180
75
@@ -192,6 +87,9 @@
192 {cite(patp)}
87 {cite(patp)}
193 </TooltipAndDocLink>
88 </TooltipAndDocLink>
194 </h2>
89 </h2>
90+ <!-- {#if (nickname)}
91+ <h3 class="text-lg text-center text-gray-800">{nickname}</h3>
92+ {/if} -->
195 {#if (!azPoint)}
93 {#if (!azPoint)}
196 <h3 class="text-lg text-center text-gray-700">{patp}</h3>
94 <h3 class="text-lg text-center text-gray-700">{patp}</h3>
197 {/if}
95 {/if}
@@ -243,10 +141,10 @@
243 {:then}
141 {:then}
244 {#if shipClass !== 'galaxy'}
142 {#if shipClass !== 'galaxy'}
245 <p>
143 <p>
246− {#if sponsorChain && sponsorChain.length > 0}
144+ {#if pointInfo.sponsorChain && pointInfo.sponsorChain.length > 0}
247 <p>
145 <p>
248− {sponsorChain.length === 1 ? 'Sponsor:' : 'Sponsor Chain:'}
146+ {pointInfo.sponsorChain.length === 1 ? 'Sponsor:' : 'Sponsor Chain:'}
249− <ShipChain shipChain={sponsorChain}></ShipChain>
147+ <ShipChain shipChain={pointInfo.sponsorChain}></ShipChain>
250 </p>
148 </p>
251 {:else}
149 {:else}
252 Unsponsored
150 Unsponsored
@@ -328,6 +226,10 @@
328 </div>
226 </div>
329 </div>
227 </div>
330 </div>
228 </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>
331 {#if canSpawnPoints}
233 {#if canSpawnPoints}
332 <div class="mt-5 p-3 border-t">
234 <div class="mt-5 p-3 border-t">
333 {#await rawPointInfoPromise then}
235 {#await rawPointInfoPromise then}
ui/src/components/Sigil.svelte+5−1
@@ -12,9 +12,13 @@
12 export let fgColor: string = 'white';
12 export let fgColor: string = 'white';
13 export let bgColor: string = 'black';
13 export let bgColor: string = 'black';
14 export let altBgColor: string = undefined;
14 export let altBgColor: string = undefined;
15+ export let noShrink: boolean = false;
15 let svgString: any;
16 let svgString: any;
16 let imgSrc: any;
17 let imgSrc: any;
17
18
19+ $: noShrinkClasses = noShrink ? 'shrink-0' : '';
20+ $: noShrinkWidth = noShrink ? size+'px' : 'initial';
21+
18 $: shipClass = clan(patp);
22 $: shipClass = clan(patp);
19 $: displaySigil = ['galaxy', 'star', 'planet'].includes(shipClass);
23 $: displaySigil = ['galaxy', 'star', 'planet'].includes(shipClass);
20 $: {
24 $: {
@@ -52,7 +56,7 @@
52 }
56 }
53</script>
57</script>
54
58
55−<div class="grow" style:max-width="{size}px">
59+<div class="grow {noShrinkClasses}" style:max-width="{size}px" style:width={noShrinkWidth}>
56 {#if displaySigil}
60 {#if displaySigil}
57 {@html svgString}
61 {@html svgString}
58 {:else}
62 {: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);
10api.ship = window.ship;
10api.ship = window.ship;
11// api.connect();
11// api.connect();
12
12
13+export { api };
14+
13export function scry<T = any>(path: string): Promise<T> {
15export function scry<T = any>(path: string): Promise<T> {
14 return api.scry<T>({ app: 'astrolabe', path });
16 return api.scry<T>({ app: 'astrolabe', path });
15}
17}
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 {
123 }
123 }
124 });
124 });
125 } catch (e) {
125 } 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);
127 this.clan = 'planet';
127 this.clan = 'planet';
128 symbols = [];
128 symbols = [];
129 }
129 }
ui/src/lib/utils.ts+10
@@ -45,3 +45,13 @@ export function toggleStoreKey(store, key) {
45export function capitalize(string: string): string {
45export function capitalize(string: string): string {
46 return string.charAt(0).toUpperCase() + string.slice(1);
46 return string.charAt(0).toUpperCase() + string.slice(1);
47}
47}
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';
3import type { Rolodex } from '@urbit/api';
3import type { Rolodex } from '@urbit/api';
4import { subscribeToContacts, getPeers } from 'lib/api';
4import { subscribeToContacts, getPeers } from 'lib/api';
5import type { StoreState } from 'types/store';
5import type { StoreState } from 'types/store';
6−import { normalizeId } from 'lib/id';
6+import { normalizeId } from 'lib/id';
7−import { setStoreKey } from 'lib/utils';
7+import { setStoreKey } from 'lib/utils';
8+import { getPals, getPalStatus, getPal } from 'lib/pals';
8
9
9const initStore: StoreState = {
10const initStore: StoreState = {
10 contacts: {},
11 contacts: {},
11 peers: [],
12 peers: [],
13+ pals: {
14+ incoming: {},
15+ outgoing: {},
16+ mutuals: {},
17+ },
18+ palsInstalled: false,
12 patpQuery: null,
19 patpQuery: null,
13 sigilQuery: null,
20 sigilQuery: null,
14 searchMode: 'patp',
21 searchMode: 'patp',
@@ -48,4 +55,12 @@ getPeers().then(({ points }) => {
48 setStoreKey(store, 'peers', points.map(normalizeId));
55 setStoreKey(store, 'peers', points.map(normalizeId));
49});
56});
50
57
58+function refreshPals() {
59+ getPals().then((pals) => {
60+ setStoreKey(store, 'pals', pals);
61+ setStoreKey(store, 'palsInstalled', true);
62+ });
63+}
64+refreshPals();
65+
51−export default { subscribe, set, searchPatp, searchSigil, reset };
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.
571.col-span-2 {
571.col-span-2 {
572 grid-column: span 2 / span 2;
572 grid-column: span 2 / span 2;
573}
573}
574+.-m-1 {
575+ margin: -0.25rem;
576+}
574.m-4 {
577.m-4 {
575 margin: 1rem;
578 margin: 1rem;
576}
579}
@@ -633,6 +636,10 @@ Ensure the default browser behavior of the `hidden` attribute.
633 margin-top: auto;
636 margin-top: auto;
634 margin-bottom: auto;
637 margin-bottom: auto;
635}
638}
639+.-mx-1 {
640+ margin-left: -0.25rem;
641+ margin-right: -0.25rem;
642+}
636.mt-2 {
643.mt-2 {
637 margin-top: 0.5rem;
644 margin-top: 0.5rem;
638}
645}
@@ -645,8 +652,8 @@ Ensure the default browser behavior of the `hidden` attribute.
645.mr-4 {
652.mr-4 {
646 margin-right: 1rem;
653 margin-right: 1rem;
647}
654}
648−.ml-4 {
655+.mr-1 {
649− margin-left: 1rem;
656+ margin-right: 0.25rem;
650}
657}
651.mt-4 {
658.mt-4 {
652 margin-top: 1rem;
659 margin-top: 1rem;
@@ -657,6 +664,9 @@ Ensure the default browser behavior of the `hidden` attribute.
657.ml-8 {
664.ml-8 {
658 margin-left: 2rem;
665 margin-left: 2rem;
659}
666}
667+.ml-4 {
668+ margin-left: 1rem;
669+}
660.-mt-2 {
670.-mt-2 {
661 margin-top: -0.5rem;
671 margin-top: -0.5rem;
662}
672}
@@ -699,8 +709,20 @@ Ensure the default browser behavior of the `hidden` attribute.
699.mb-1 {
709.mb-1 {
700 margin-bottom: 0.25rem;
710 margin-bottom: 0.25rem;
701}
711}
702−.mr-1 {
712+.mr-2 {
703− margin-right: 0.25rem;
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;
704}
726}
705.-mt-0\.5 {
727.-mt-0\.5 {
706 margin-top: -0.125rem;
728 margin-top: -0.125rem;
@@ -741,6 +763,11 @@ Ensure the default browser behavior of the `hidden` attribute.
741.h-12 {
763.h-12 {
742 height: 3rem;
764 height: 3rem;
743}
765}
766+.h-fit {
767+ height: -webkit-fit-content;
768+ height: -moz-fit-content;
769+ height: fit-content;
770+}
744.h-3 {
771.h-3 {
745 height: 0.75rem;
772 height: 0.75rem;
746}
773}
@@ -789,21 +816,40 @@ Ensure the default browser behavior of the `hidden` attribute.
789.h-auto {
816.h-auto {
790 height: auto;
817 height: auto;
791}
818}
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+}
792.min-h-\[20px\] {
836.min-h-\[20px\] {
793 min-height: 20px;
837 min-height: 20px;
794}
838}
795.min-h-full {
839.min-h-full {
796 min-height: 100%;
840 min-height: 100%;
797}
841}
798−.min-h-0 {
799− min-height: 0px;
800−}
801.min-h-\[14rem\] {
842.min-h-\[14rem\] {
802 min-height: 14rem;
843 min-height: 14rem;
803}
844}
804.w-full {
845.w-full {
805 width: 100%;
846 width: 100%;
806}
847}
848+.w-min {
849+ width: -webkit-min-content;
850+ width: -moz-min-content;
851+ width: min-content;
852+}
807.w-5 {
853.w-5 {
808 width: 1.25rem;
854 width: 1.25rem;
809}
855}
@@ -858,12 +904,25 @@ Ensure the default browser behavior of the `hidden` attribute.
858.w-40 {
904.w-40 {
859 width: 10rem;
905 width: 10rem;
860}
906}
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+}
861.w-\[48px\] {
917.w-\[48px\] {
862 width: 48px;
918 width: 48px;
863}
919}
864.min-w-0 {
920.min-w-0 {
865 min-width: 0px;
921 min-width: 0px;
866}
922}
923+.min-w-\[128px\] {
924+ min-width: 128px;
925+}
867.max-w-md {
926.max-w-md {
868 max-width: 28rem;
927 max-width: 28rem;
869}
928}
@@ -882,6 +941,15 @@ Ensure the default browser behavior of the `hidden` attribute.
882.max-w-2xl {
941.max-w-2xl {
883 max-width: 42rem;
942 max-width: 42rem;
884}
943}
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+}
885.max-w-\[10rem\] {
953.max-w-\[10rem\] {
886 max-width: 10rem;
954 max-width: 10rem;
887}
955}
@@ -900,6 +968,9 @@ Ensure the default browser behavior of the `hidden` attribute.
900.shrink {
968.shrink {
901 flex-shrink: 1;
969 flex-shrink: 1;
902}
970}
971+.shrink-0 {
972+ flex-shrink: 0;
973+}
903.flex-grow {
974.flex-grow {
904 flex-grow: 1;
975 flex-grow: 1;
905}
976}
@@ -1010,6 +1081,9 @@ Ensure the default browser behavior of the `hidden` attribute.
1010.grid-flow-col {
1081.grid-flow-col {
1011 grid-auto-flow: column;
1082 grid-auto-flow: column;
1012}
1083}
1084+.grid-flow-row-dense {
1085+ grid-auto-flow: row dense;
1086+}
1013.grid-flow-col-dense {
1087.grid-flow-col-dense {
1014 grid-auto-flow: column dense;
1088 grid-auto-flow: column dense;
1015}
1089}
@@ -1038,6 +1112,9 @@ Ensure the default browser behavior of the `hidden` attribute.
1038.grid-rows-none {
1112.grid-rows-none {
1039 grid-template-rows: none;
1113 grid-template-rows: none;
1040}
1114}
1115+.grid-rows-3 {
1116+ grid-template-rows: repeat(3, minmax(0, 1fr));
1117+}
1041.flex-col {
1118.flex-col {
1042 flex-direction: column;
1119 flex-direction: column;
1043}
1120}
@@ -1065,9 +1142,18 @@ Ensure the default browser behavior of the `hidden` attribute.
1065.justify-center {
1142.justify-center {
1066 justify-content: center;
1143 justify-content: center;
1067}
1144}
1145+.justify-between {
1146+ justify-content: space-between;
1147+}
1148+.justify-evenly {
1149+ justify-content: space-evenly;
1150+}
1068.justify-items-center {
1151.justify-items-center {
1069 justify-items: center;
1152 justify-items: center;
1070}
1153}
1154+.justify-items-stretch {
1155+ justify-items: stretch;
1156+}
1071.gap-5 {
1157.gap-5 {
1072 gap: 1.25rem;
1158 gap: 1.25rem;
1073}
1159}
@@ -1083,6 +1169,13 @@ Ensure the default browser behavior of the `hidden` attribute.
1083.gap-2 {
1169.gap-2 {
1084 gap: 0.5rem;
1170 gap: 0.5rem;
1085}
1171}
1172+.gap-3 {
1173+ gap: 0.75rem;
1174+}
1175+.gap-x-4 {
1176+ -moz-column-gap: 1rem;
1177+ column-gap: 1rem;
1178+}
1086.gap-x-1 {
1179.gap-x-1 {
1087 -moz-column-gap: 0.25rem;
1180 -moz-column-gap: 0.25rem;
1088 column-gap: 0.25rem;
1181 column-gap: 0.25rem;
@@ -1095,10 +1188,6 @@ Ensure the default browser behavior of the `hidden` attribute.
1095 -moz-column-gap: 0.75rem;
1188 -moz-column-gap: 0.75rem;
1096 column-gap: 0.75rem;
1189 column-gap: 0.75rem;
1097}
1190}
1098−.gap-x-4 {
1099− -moz-column-gap: 1rem;
1100− column-gap: 1rem;
1101−}
1102.space-y-4 > :not([hidden]) ~ :not([hidden]) {
1191.space-y-4 > :not([hidden]) ~ :not([hidden]) {
1103 --tw-space-y-reverse: 0;
1192 --tw-space-y-reverse: 0;
1104 margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
1193 margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
@@ -1138,6 +1227,38 @@ Ensure the default browser behavior of the `hidden` attribute.
1138 --tw-divide-opacity: 1;
1227 --tw-divide-opacity: 1;
1139 border-color: rgb(252 211 77 / var(--tw-divide-opacity));
1228 border-color: rgb(252 211 77 / var(--tw-divide-opacity));
1140}
1229}
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+}
1141.place-self-center {
1262.place-self-center {
1142 place-self: center;
1263 place-self: center;
1143}
1264}
@@ -1165,6 +1286,13 @@ Ensure the default browser behavior of the `hidden` attribute.
1165.overflow-y-auto {
1286.overflow-y-auto {
1166 overflow-y: auto;
1287 overflow-y: auto;
1167}
1288}
1289+.whitespace-pre-line {
1290+ white-space: pre-line;
1291+}
1292+.break-normal {
1293+ overflow-wrap: normal;
1294+ word-break: normal;
1295+}
1168.break-words {
1296.break-words {
1169 overflow-wrap: break-word;
1297 overflow-wrap: break-word;
1170}
1298}
@@ -1209,6 +1337,12 @@ Ensure the default browser behavior of the `hidden` attribute.
1209.rounded-bl-none {
1337.rounded-bl-none {
1210 border-bottom-left-radius: 0px;
1338 border-bottom-left-radius: 0px;
1211}
1339}
1340+.rounded-tl-lg {
1341+ border-top-left-radius: 0.5rem;
1342+}
1343+.rounded-br-lg {
1344+ border-bottom-right-radius: 0.5rem;
1345+}
1212.border {
1346.border {
1213 border-width: 1px;
1347 border-width: 1px;
1214}
1348}
@@ -1242,6 +1376,17 @@ Ensure the default browser behavior of the `hidden` attribute.
1242.border-l {
1376.border-l {
1243 border-left-width: 1px;
1377 border-left-width: 1px;
1244}
1378}
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+}
1245.border-gold {
1390.border-gold {
1246 --tw-border-opacity: 1;
1391 --tw-border-opacity: 1;
1247 border-color: rgb(252 211 77 / var(--tw-border-opacity));
1392 border-color: rgb(252 211 77 / var(--tw-border-opacity));
@@ -1262,10 +1407,6 @@ Ensure the default browser behavior of the `hidden` attribute.
1262 --tw-border-opacity: 1;
1407 --tw-border-opacity: 1;
1263 border-color: rgb(0 0 0 / var(--tw-border-opacity));
1408 border-color: rgb(0 0 0 / var(--tw-border-opacity));
1264}
1409}
1265−.border-gray-400 {
1266− --tw-border-opacity: 1;
1267− border-color: rgb(156 163 175 / var(--tw-border-opacity));
1268−}
1269.border-red-500 {
1410.border-red-500 {
1270 --tw-border-opacity: 1;
1411 --tw-border-opacity: 1;
1271 border-color: rgb(239 68 68 / var(--tw-border-opacity));
1412 border-color: rgb(239 68 68 / var(--tw-border-opacity));
@@ -1286,14 +1427,18 @@ Ensure the default browser behavior of the `hidden` attribute.
1286 --tw-border-opacity: 1;
1427 --tw-border-opacity: 1;
1287 border-color: rgb(31 41 55 / var(--tw-border-opacity));
1428 border-color: rgb(31 41 55 / var(--tw-border-opacity));
1288}
1429}
1289−.border-gray-500 {
1430+.border-gray-600 {
1290 --tw-border-opacity: 1;
1431 --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));
1292}
1433}
1293.bg-navy {
1434.bg-navy {
1294 --tw-bg-opacity: 1;
1435 --tw-bg-opacity: 1;
1295 background-color: rgb(31 36 60 / var(--tw-bg-opacity));
1436 background-color: rgb(31 36 60 / var(--tw-bg-opacity));
1296}
1437}
1438+.bg-gray-400 {
1439+ --tw-bg-opacity: 1;
1440+ background-color: rgb(156 163 175 / var(--tw-bg-opacity));
1441+}
1297.bg-gold {
1442.bg-gold {
1298 --tw-bg-opacity: 1;
1443 --tw-bg-opacity: 1;
1299 background-color: rgb(252 211 77 / var(--tw-bg-opacity));
1444 background-color: rgb(252 211 77 / var(--tw-bg-opacity));
@@ -1322,6 +1467,10 @@ Ensure the default browser behavior of the `hidden` attribute.
1322 --tw-bg-opacity: 1;
1467 --tw-bg-opacity: 1;
1323 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
1468 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
1324}
1469}
1470+.bg-gray-200 {
1471+ --tw-bg-opacity: 1;
1472+ background-color: rgb(229 231 235 / var(--tw-bg-opacity));
1473+}
1325.bg-black {
1474.bg-black {
1326 --tw-bg-opacity: 1;
1475 --tw-bg-opacity: 1;
1327 background-color: rgb(0 0 0 / var(--tw-bg-opacity));
1476 background-color: rgb(0 0 0 / var(--tw-bg-opacity));
@@ -1334,10 +1483,6 @@ Ensure the default browser behavior of the `hidden` attribute.
1334 --tw-bg-opacity: 1;
1483 --tw-bg-opacity: 1;
1335 background-color: rgb(253 224 71 / var(--tw-bg-opacity));
1484 background-color: rgb(253 224 71 / var(--tw-bg-opacity));
1336}
1485}
1337−.bg-gray-400 {
1338− --tw-bg-opacity: 1;
1339− background-color: rgb(156 163 175 / var(--tw-bg-opacity));
1340−}
1341.bg-orange-300 {
1486.bg-orange-300 {
1342 --tw-bg-opacity: 1;
1487 --tw-bg-opacity: 1;
1343 background-color: rgb(253 186 116 / var(--tw-bg-opacity));
1488 background-color: rgb(253 186 116 / var(--tw-bg-opacity));
@@ -1362,10 +1507,6 @@ Ensure the default browser behavior of the `hidden` attribute.
1362 --tw-bg-opacity: 1;
1507 --tw-bg-opacity: 1;
1363 background-color: rgb(253 230 138 / var(--tw-bg-opacity));
1508 background-color: rgb(253 230 138 / var(--tw-bg-opacity));
1364}
1509}
1365−.bg-gray-200 {
1366− --tw-bg-opacity: 1;
1367− background-color: rgb(229 231 235 / var(--tw-bg-opacity));
1368−}
1369.bg-gold-t8 {
1510.bg-gold-t8 {
1370 --tw-bg-opacity: 1;
1511 --tw-bg-opacity: 1;
1371 background-color: rgb(254 246 219 / var(--tw-bg-opacity));
1512 background-color: rgb(254 246 219 / var(--tw-bg-opacity));
@@ -1414,9 +1555,24 @@ Ensure the default browser behavior of the `hidden` attribute.
1414 --tw-bg-opacity: 1;
1555 --tw-bg-opacity: 1;
1415 background-color: rgb(107 114 128 / var(--tw-bg-opacity));
1556 background-color: rgb(107 114 128 / var(--tw-bg-opacity));
1416}
1557}
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+}
1417.fill-transparent {
1566.fill-transparent {
1418 fill: transparent;
1567 fill: transparent;
1419}
1568}
1569+.object-cover {
1570+ -o-object-fit: cover;
1571+ object-fit: cover;
1572+}
1573+.p-1 {
1574+ padding: 0.25rem;
1575+}
1420.p-5 {
1576.p-5 {
1421 padding: 1.25rem;
1577 padding: 1.25rem;
1422}
1578}
@@ -1435,9 +1591,6 @@ Ensure the default browser behavior of the `hidden` attribute.
1435.p-6 {
1591.p-6 {
1436 padding: 1.5rem;
1592 padding: 1.5rem;
1437}
1593}
1438−.p-1 {
1439− padding: 0.25rem;
1440−}
1441.p-0 {
1594.p-0 {
1442 padding: 0px;
1595 padding: 0px;
1443}
1596}
@@ -1559,6 +1712,10 @@ Ensure the default browser behavior of the `hidden` attribute.
1559 font-size: 1.875rem;
1712 font-size: 1.875rem;
1560 line-height: 2.25rem;
1713 line-height: 2.25rem;
1561}
1714}
1715+.text-sm {
1716+ font-size: 0.875rem;
1717+ line-height: 1.25rem;
1718+}
1562.text-xl {
1719.text-xl {
1563 font-size: 1.25rem;
1720 font-size: 1.25rem;
1564 line-height: 1.75rem;
1721 line-height: 1.75rem;
@@ -1571,10 +1728,6 @@ Ensure the default browser behavior of the `hidden` attribute.
1571 font-size: 1.125rem;
1728 font-size: 1.125rem;
1572 line-height: 1.75rem;
1729 line-height: 1.75rem;
1573}
1730}
1574−.text-sm {
1575− font-size: 0.875rem;
1576− line-height: 1.25rem;
1577−}
1578.text-base {
1731.text-base {
1579 font-size: 1rem;
1732 font-size: 1rem;
1580 line-height: 1.5rem;
1733 line-height: 1.5rem;
@@ -1598,6 +1751,14 @@ Ensure the default browser behavior of the `hidden` attribute.
1598 --tw-text-opacity: 1;
1751 --tw-text-opacity: 1;
1599 color: rgb(255 255 255 / var(--tw-text-opacity));
1752 color: rgb(255 255 255 / var(--tw-text-opacity));
1600}
1753}
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+}
1601.text-gray-700 {
1762.text-gray-700 {
1602 --tw-text-opacity: 1;
1763 --tw-text-opacity: 1;
1603 color: rgb(55 65 81 / var(--tw-text-opacity));
1764 color: rgb(55 65 81 / var(--tw-text-opacity));
@@ -1610,18 +1771,10 @@ Ensure the default browser behavior of the `hidden` attribute.
1610 --tw-text-opacity: 1;
1771 --tw-text-opacity: 1;
1611 color: rgb(22 163 74 / var(--tw-text-opacity));
1772 color: rgb(22 163 74 / var(--tw-text-opacity));
1612}
1773}
1613−.text-gray-800 {
1614− --tw-text-opacity: 1;
1615− color: rgb(31 41 55 / var(--tw-text-opacity));
1616−}
1617.text-gray-500 {
1774.text-gray-500 {
1618 --tw-text-opacity: 1;
1775 --tw-text-opacity: 1;
1619 color: rgb(107 114 128 / var(--tw-text-opacity));
1776 color: rgb(107 114 128 / var(--tw-text-opacity));
1620}
1777}
1621−.text-gray-600 {
1622− --tw-text-opacity: 1;
1623− color: rgb(75 85 99 / var(--tw-text-opacity));
1624−}
1625.text-green-500 {
1778.text-green-500 {
1626 --tw-text-opacity: 1;
1779 --tw-text-opacity: 1;
1627 color: rgb(34 197 94 / var(--tw-text-opacity));
1780 color: rgb(34 197 94 / var(--tw-text-opacity));
@@ -1638,6 +1791,10 @@ Ensure the default browser behavior of the `hidden` attribute.
1638 --tw-text-opacity: 1;
1791 --tw-text-opacity: 1;
1639 color: rgb(252 211 77 / var(--tw-text-opacity));
1792 color: rgb(252 211 77 / var(--tw-text-opacity));
1640}
1793}
1794+.text-gray-300 {
1795+ --tw-text-opacity: 1;
1796+ color: rgb(209 213 219 / var(--tw-text-opacity));
1797+}
1641.underline {
1798.underline {
1642 -webkit-text-decoration-line: underline;
1799 -webkit-text-decoration-line: underline;
1643 text-decoration-line: underline;
1800 text-decoration-line: underline;
@@ -1816,6 +1973,10 @@ Ensure the default browser behavior of the `hidden` attribute.
1816 margin-right: auto;
1973 margin-right: auto;
1817 }
1974 }
1818
1975
1976+ .md\:w-1\/2 {
1977+ width: 50%;
1978+ }
1979+
1819 .md\:min-w-\[256px\] {
1980 .md\:min-w-\[256px\] {
1820 min-width: 256px;
1981 min-width: 256px;
1821 }
1982 }
@@ -1832,6 +1993,10 @@ Ensure the default browser behavior of the `hidden` attribute.
1832 flex-direction: row;
1993 flex-direction: row;
1833 }
1994 }
1834
1995
1996+ .md\:flex-nowrap {
1997+ flex-wrap: nowrap;
1998+ }
1999+
1835 .md\:items-start {
2000 .md\:items-start {
1836 align-items: flex-start;
2001 align-items: flex-start;
1837 }
2002 }
ui/src/types/store.ts+29
@@ -7,6 +7,33 @@ export interface Contacts {
7export interface Docs {
7export interface Docs {
8 [doc: string]: string;
8 [doc: string]: string;
9};
9};
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+
10export interface StoreState {
37export interface StoreState {
11 // Urbit
38 // Urbit
12 // groups: {
39 // groups: {
@@ -14,6 +41,8 @@ export interface StoreState {
14 // };
41 // };
15 contacts: Rolodex;
42 contacts: Rolodex;
16 peers: Patp[];
43 peers: Patp[];
44+ pals: Pals;
45+ palsInstalled: boolean;
17 patpQuery: RegExp;
46 patpQuery: RegExp;
18 sigilQuery: string[][];
47 sigilQuery: string[][];
19 searchMode: ('patp' | 'sigil');
48 searchMode: ('patp' | 'sigil');