on main help sign in
basic ship view without pokes or scries or nothin
commit508d0d9bbe6b4cf23f7c0fac616cd449e63c169c authorJohn Hyde <johnhydefour@gmail.com> dateThu 17 Mar 2022, 15:38 (5 years ago) parent51d5e88
33 files changed+741−138
README.md+1−1 ui/.env.local+2−1 ui/.gitignore+1 ui/index.html+1−1 ui/package-lock.json+8−7 ui/package.json+3−2 ui/scraps/very pointy ideas.css+8 ui/src/App.svelte+11−3 ui/src/assets/comet.svg+19 ui/src/assets/moon.png ui/src/components/Home.svelte−18 ui/src/components/SearchBar.svelte+23−27 ui/src/components/SearchResultNavButtons.svelte+61 ui/src/components/SearchResults.svelte+60−2 ui/src/components/ShipListing.svelte+16 ui/src/components/ShipView.svelte+54−8 ui/src/components/ShipViewPage.svelte−6 ui/src/components/Sigil.svelte+36 ui/src/components/ValidationProblems.svelte+19 ui/src/fake_data/fake-contacts.ts+92 ui/src/lib/id.ts+102−20 ui/src/lib/link.ts+3 ui/src/lib/utils.ts+7 ui/src/routes/Home.svelte+11 ui/src/routes/ShipViewPage.svelte+26 ui/src/stores/store.ts+40 ui/src/styles/tailwind-output.css+107−35 ui/src/styles/tailwind.css+1−1 ui/src/types/connection.ts+1 ui/src/types/store.ts+16 ui/svelte.config.js+7−1 ui/tailwind.config.cjs+4−5 ui/tsconfig.json+1
README.md+1−1
@@ -6,7 +6,7 @@ There should be a place on Urbit to explore Urbit address space. It will help ne
66I like the name _Astrolabe_, because it's an old-timey guide to the heavenly bodies. _Planisphere_ could also be cool.
77
88# Proposal
9−To read and discuss the full proposal, join the dedicated group on Urbit: `~poster-hoster-midlev-mindyr/astrolabe`
9+You can read about the proposal [here](https://urbit.org/grants/astrolabe), and discuss in the dedicated group on Urbit: `~poster-hoster-midlev-mindyr/astrolabe`
1010
1111# The Code
1212## Desk
ui/.env.local+2−1
@@ -1 +1,2 @@
1−VITE_SHIP_URL=http://localhost:8080
1+# VITE_SHIP_URL=http://localhost:8081
2+VITE_SHIP_URL=http://100.121.123.89:8081
ui/.gitignore+1
@@ -2,3 +2,4 @@
22/dist/
33/.vscode/
44.DS_Store
5+.env.local
ui/index.html+1−1
@@ -4,7 +4,7 @@
44 <meta charset="UTF-8" />
55 <link rel="icon" href="/favicon.ico" />
66 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7− <title>Svelte + TS + Vite App</title>
7+ <title>Astrolabe</title>
88 </head>
99 <body>
1010 <div id="app"></div>
ui/package-lock.json+8−7
@@ -11,7 +11,8 @@
1111 "@tlon/phonemes-js": "^1.0.0",
1212 "@tlon/sigil-js": "^1.4.5",
1313 "@urbit/api": "^2.1.0",
14− "@urbit/http-api": "^2.1.0"
14+ "@urbit/http-api": "^2.1.0",
15+ "urbit-ob": "^5.0.1"
1516 },
1617 "devDependencies": {
1718 "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30",
@@ -1696,9 +1697,9 @@
16961697 "dev": true
16971698 },
16981699 "node_modules/nanoid": {
1699− "version": "3.1.30",
1700− "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
1701− "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==",
1700+ "version": "3.3.1",
1701+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
1702+ "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
17021703 "dev": true,
17031704 "bin": {
17041705 "nanoid": "bin/nanoid.cjs"
@@ -4072,9 +4073,9 @@
40724073 "dev": true
40734074 },
40744075 "nanoid": {
4075− "version": "3.1.30",
4076− "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz",
4077− "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==",
4076+ "version": "3.3.1",
4077+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
4078+ "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
40784079 "dev": true
40794080 },
40804081 "node-releases": {
ui/package.json+3−2
@@ -3,7 +3,7 @@
33 "version": "0.0.0",
44 "type": "module",
55 "scripts": {
6− "dev:only": "vite",
6+ "dev:only": "vite --port 4000",
77 "build:only": "vite build",
88 "tailwind:watch": "cross-env TAILWIND_MODE=watch cross-env NODE_ENV=development postcss src/styles/tailwind.css -o src/styles/tailwind-output.css -w",
99 "tailwind:build": "cross-env TAILWIND_MODE=build cross-env NODE_ENV=production postcss src/styles/tailwind.css -o src/styles/tailwind-output.css",
@@ -33,6 +33,7 @@
3333 "@tlon/phonemes-js": "^1.0.0",
3434 "@tlon/sigil-js": "^1.4.5",
3535 "@urbit/api": "^2.1.0",
36− "@urbit/http-api": "^2.1.0"
36+ "@urbit/http-api": "^2.1.0",
37+ "urbit-ob": "^5.0.1"
3738 }
3839}
ui/scraps/very pointy ideas.cssnew file+8
@@ -0,0 +1,8 @@
1+very pointy
2+clip-path: polygon(25% 0%, 0% 50%, 25% 100%, 100% 100%, 75% 50%, 100% 0%);
3+
4+less pointy
5+clip-path: polygon(25% 0%, 0% 50%, 25% 100%, 100% 100%, 100% 0%);
6+
7+sharper point
8+clip-path: polygon(100% 0%, 0% 50%, 100% 100%, 100% 100%, 100% 0%);
ui/src/App.svelte+11−3
@@ -2,8 +2,8 @@
22 import Router from 'svelte-spa-router';
33 import Urbit from '@urbit/http-api';
44 import { Charges, ChargeUpdateInitial, scryCharges } from '@urbit/api';
5− import Home from './components/Home.svelte';
6− import ShipViewPage from './components/ShipViewPage.svelte';
5+ import Home from './routes/Home.svelte';
6+ import ShipViewPage from './routes/ShipViewPage.svelte';
77 import "./styles/tailwind-output.css";
88
99 const routes = {
@@ -13,6 +13,14 @@
1313 }
1414</script>
1515
16−<main class="h-screen bg-green-600">
16+<main>
1717 <Router {routes} />
1818</main>
19+
20+<svelte:head>
21+ <style>
22+ body {
23+ background-color: #1f243c;
24+ }
25+ </style>
26+</svelte:head>
ui/src/assets/comet.svgnew file+19
@@ -0,0 +1,19 @@
1+<svg version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 612 612"><g><path d="M503.694,7.871c1.344-2.047,1.047-4.781-0.734-6.453c-1.781-1.703-4.5-1.891-6.484-0.438l-98.547,71.344
2+ c-1.891,1.359-4.484,1.266-6.266-0.25s-2.313-4.047-1.266-6.141l10.844-21.656c1.031-2.047,0.547-4.547-1.156-6.063
3+ c-1.719-1.531-4.25-1.719-6.172-0.469L83.006,230.137c-3.156,1.813-6.297,3.688-9.328,5.75l-0.25,0.156l0.016,0.016
4+ c-7.625,5.219-14.922,11.125-21.688,17.891c-59.047,59.031-59.047,154.75-0.016,213.766
5+ c29.531,29.516,68.219,44.281,106.906,44.281s77.359-14.766,106.891-44.281c6.766-6.766,12.656-14.047,17.875-21.672v0.016
6+ l0.109-0.172c1.844-2.703,3.516-5.484,5.172-8.281l188.375-302.078c1.203-1.844,1.094-4.25-0.281-5.953
7+ c-1.375-1.734-3.688-2.375-5.75-1.594l-46.829,17.563c-2.063,0.75-4.375,0.125-5.75-1.594s-1.484-4.125-0.266-5.953L503.694,7.871z
8+ M158.647,464.73c-27.766,0-53.859-10.797-73.484-30.438c-40.5-40.5-40.5-106.406,0-146.922
9+ c6.813-6.797,14.422-12.469,22.578-17.063c7.406-4.172,15.297-7.391,23.5-9.641c0.766-0.203,1.547-0.375,2.344-0.578
10+ c3.172-0.797,6.406-1.422,9.672-1.906c1.031-0.156,2.047-0.328,3.078-0.453c4.047-0.484,8.156-0.797,12.313-0.797
11+ c27.75,0,53.828,10.813,73.453,30.438c2.344,2.328,4.516,4.781,6.578,7.281c0.688,0.813,1.297,1.672,1.938,2.5
12+ c1.344,1.734,2.641,3.469,3.859,5.25c0.703,1.031,1.359,2.063,2.016,3.109c1.047,1.656,2.047,3.344,3,5.063
13+ c0.609,1.109,1.219,2.234,1.797,3.359c0.859,1.719,1.656,3.484,2.422,5.234c0.5,1.141,1.031,2.266,1.484,3.422
14+ c0.813,2.063,1.516,4.172,2.188,6.266c0.656,2.031,1.219,4.063,1.75,6.125c0.391,1.563,0.813,3.109,1.141,4.688
15+ c0.344,1.703,0.609,3.438,0.875,5.156c0.188,1.219,0.375,2.438,0.516,3.656c0.219,1.875,0.391,3.75,0.5,5.625
16+ c0.063,1.016,0.109,2.047,0.156,3.063c0.063,2.047,0.109,4.094,0.047,6.141c-0.016,0.656-0.063,1.328-0.094,1.984
17+ c-0.453,10.406-2.438,20.766-6,30.688c-0.063,0.219-0.141,0.438-0.219,0.641c-0.891,2.422-1.875,4.813-2.938,7.188
18+ c-0.125,0.266-0.266,0.563-0.391,0.828c-1.125,2.406-2.313,4.781-3.609,7.094c-4.547,8.078-10.141,15.703-17,22.563
19+ C212.475,453.934,186.397,464.73,158.647,464.73z" style="fill: var(--foreground);"></path></g></svg>
ui/src/assets/moon.pngnew file

Binary file not shown.

ui/src/components/Home.sveltedeleted−18
@@ -1,18 +0,0 @@
1−<script lang="ts">
2− import SearchBar from "./SearchBar.svelte";
3− import SearchResults from "./SearchResults.svelte";
4− let search: string = '';
5−</script>
6−
7−<SearchBar bind:search />
8−{#if search == ''}
9− <p>
10− Hello, ~{window.ship}, would you like to search?
11− </p>
12−{:else}
13− <SearchResults {search} />
14−{/if}
15−
16−<footer>
17− Made by <a href="#/ship/~midlev-mindyr">~midlev-mindyr</a>.
18−</footer>
ui/src/components/SearchBar.svelte+23−27
@@ -1,39 +1,35 @@
11<script lang="ts">
2− import { validateId } from '../lib/id';
2+ import ValidationErrors from './ValidationProblems.svelte';
33
4− export let search: string = "";
4+ import { validateIdSearchText, convertIdSearchTextToQuery } from '../lib/id';
5+
6+ export let searchText: string = 'der';
7+ export let searchQuery: RegExp = /~der/;
58 let problems: Array<string> = [];
69
7− function updateSearch(e) {
8− let newSearch = e.target.value;
9− problems = validateId(newSearch);
10+ $: {
11+ problems = validateIdSearchText(searchText);
1012 if (problems.length === 0) {
11− search = newSearch;
13+ searchQuery = convertIdSearchTextToQuery(searchText);
14+ } else {
15+ searchQuery = undefined;
1216 }
1317 }
1418</script>
1519
1620<div class="flex justify-center">
17− <input type="text" class="m-8 px-4 py-2 grow max-w-md rounded-full"
18− placeholder="Enter an ID or Azimuth number. '*' is wildcard."
19− value={search}
20− on:change={updateSearch}
21− />
21+ <div class="relative grow max-w-md rounded-full">
22+ <input type="text" class="p-4 py-2 w-full rounded-full"
23+ spellcheck="false"
24+ placeholder="Enter an ID or Azimuth number. '*' is wildcard."
25+ bind:value={searchText}
26+ />
27+ <button class="absolute right-0 w-10 h-10 rounded-full bg-black transition-opacity opacity-10 hover:opacity-40"></button>
28+ </div>
2229</div>
2330
24−<div class="m-8">
25− {#if problems.length > 0}
26− <p>
27− Sorry, the ID you entered is invalid:
28− </p>
29− {#each problems as problem}
30− <p>
31− {problem}
32− </p>
33− {/each}
34− <p>
35− <!-- TODO: add link to doc -->
36− You can read more here.
37− </p>
38− {/if}
39−</div>
31+{#if searchText !== '' && problems.length > 0}
32+ <div class="flex justify-center">
33+ <ValidationErrors {problems}></ValidationErrors>
34+ </div>
35+{/if}
ui/src/components/SearchResultNavButtons.sveltenew file+61
@@ -0,0 +1,61 @@
1+<script lang="ts">
2+ import { createEventDispatcher } from 'svelte';
3+ export let index: number;
4+ export let totalResults: number;
5+ const dispatch = createEventDispatcher();
6+
7+ function updateIndex(index: number) {
8+ dispatch('updateIndex', { index });
9+ }
10+
11+ function visitPrevious() {
12+ updateIndex(index - 1);
13+ }
14+
15+ function visitNext() {
16+ updateIndex(index + 1);
17+ }
18+
19+ function seeList() {
20+ updateIndex(null);
21+ }
22+</script>
23+
24+<div class="flex justify-center">
25+ <div class="grid grid-cols-3 h-6 mx-8 max-w-md w-full" >
26+ <div class="justify-self-start">
27+ {#if index > 0}
28+ <button
29+ on:click={visitPrevious}
30+ class="h-6 w-12 bg-white"
31+ style="
32+ clip-path: polygon(25% 0%, 0% 50%, 25% 100%, 100% 100%, 75% 50%, 100% 0%);
33+ "
34+ >
35+ </button>
36+ {/if}
37+ </div>
38+ <div class="text-center">
39+ <button
40+ on:click={seeList}
41+ class="-mt-2 h-8 w-12 bg-white"
42+ style="
43+ clip-path: polygon(0% 100%, 0% 66%, 50% 0%, 100% 66%, 100% 100%, 78% 100%, 50% 66%, 22% 100%);
44+ "
45+ >
46+ </button>
47+ </div>
48+ <div class="justify-self-end">
49+ {#if index < totalResults - 1}
50+ <button
51+ on:click={visitNext}
52+ class="h-6 w-12 bg-white"
53+ style="
54+ clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%);
55+ "
56+ >
57+ </button>
58+ {/if}
59+ </div>
60+ </div>
61+</div>
ui/src/components/SearchResults.svelte+60−2
@@ -1,5 +1,63 @@
11<script lang="ts">
2− export let search : string = "";
2+ import { Groups, Rolodex } from '@urbit/api';
3+ import store from '../stores/store';
4+ import ShipListing from './ShipListing.svelte';
5+ import SearchResultNavButtons from './SearchResultNavButtons.svelte';
6+ import ShipView from './ShipView.svelte';
7+ export const searchText : string = '';
8+ export let searchQuery : RegExp;
9+ let contactsList = [];
10+ let selectedShipIndex: number;
11+
12+ $: {
13+ store.search(searchQuery)
14+ // if (searchQuery) {
15+ // store.search(searchQuery)
16+ // } else {
17+ // store.reset();
18+ // }
19+ }
20+ $: {
21+ contactsList = Object.entries($store?.contacts).map(([patp, ship]) => {
22+ return {
23+ ...ship,
24+ patp,
25+ };
26+ });
27+ if (contactsList.length === 1) {
28+ selectedShipIndex = 0;
29+ } else {
30+ selectedShipIndex = null;
31+ }
32+ }
33+ $: selectedShip = contactsList[selectedShipIndex]
34+
35+ function updateIndex(e) {
36+ selectedShipIndex = e.detail.index;
37+ }
338</script>
439
5−<a href="#/ship/{search}">{search}</a>
40+<!-- TODO: find out why this doesn't update as expected -->
41+<!-- {#if searchQuery === undefined}
42+ <p class="p-6 rounded-lg bg-white">
43+ Hello, ~{window.ship}, would you like to search?
44+ </p>
45+{/if} -->
46+{#if selectedShip}
47+ {#if contactsList.length > 1}
48+ <SearchResultNavButtons
49+ index={selectedShipIndex}
50+ totalResults={contactsList.length}
51+ on:updateIndex={updateIndex}
52+ />
53+ {/if}
54+ <ShipView patp={selectedShip.patp} />
55+{:else}
56+ <div class="flex justify-center">
57+ <div class="max-w-md w-full rounded-lg bg-white">
58+ {#each contactsList as contact, i (contact.patp)}
59+ <ShipListing ship={contact} on:click={() => selectedShipIndex = i}/>
60+ {/each}
61+ </div>
62+ </div>
63+{/if}
ui/src/components/ShipListing.sveltenew file+16
@@ -0,0 +1,16 @@
1+<script lang="ts">
2+ import Sigil from "./Sigil.svelte";
3+ export let ship: any;
4+</script>
5+
6+<div class="flex flex-wrap p-4 border-b last:border-none items-center"
7+ on:click
8+ >
9+ <Sigil patp={ship.patp} size={48} />
10+ <!-- {#if ship.avatar}
11+ <img src={ship.avatar} class="h-12 w-12 mr-4" />
12+ {/if} -->
13+ <p class="ml-4 break-words" >
14+ {ship.patp}<br/>{ship.nickname ? `(${ship.nickname})` : ''}
15+ </p>
16+</div>
ui/src/components/ShipView.svelte+54−8
@@ -1,12 +1,58 @@
11<script lang="ts">
2+ import { clan, sein, patp2dec } from 'urbit-ob';
3+ import { link } from 'svelte-spa-router'
4+ import { linkToShip } from '../lib/link';
5+ import Sigil from "./Sigil.svelte";
26 export let patp: string;
7+ let parentChain: any[];
8+
9+ $: shipClass = clan(patp);
10+ $: azPoint = ['galaxy', 'star', 'planet'].includes(shipClass);
11+ $: {
12+ let parents = [patp];
13+ let galaxyFound = false;
14+ let count = 10;
15+ while (!galaxyFound && count > 0) {
16+ count--;
17+ if (count == 0) {
18+ throw new Error("You've got an infinite loop in building the parent chain");
19+ }
20+ let firstItem = parents[0];
21+ if (clan(firstItem) == 'galaxy') {
22+ galaxyFound = true;
23+ } else {
24+ parents.unshift(sein(firstItem))
25+ }
26+ }
27+ parents.pop();
28+ parentChain = parents.map(parent => {
29+ return { patp: parent, link: linkToShip(parent) };
30+ });
31+ }
332</script>
433
5−<div>
6− <p>
7− hi i'm a ship: {patp}
8− </p>
9− <footer>
10− <a href="#/" >Go Home</a>
11− </footer>
12−</div>
34+<div class="flex justify-center">
35+ <div class="p-8 max-w-md w-full rounded-lg bg-white">
36+ <div class="mx-auto">
37+ <Sigil patp={patp} size={256} />
38+ </div>
39+ <h2 class="text-2xl text-center">{patp}</h2>
40+ <p>
41+ Ship Type: {shipClass}
42+ </p>
43+ {#if azPoint}
44+ <p class="break-all">
45+ Azimuth #: {patp2dec(patp)}
46+ </p>
47+ {/if}
48+ {#if parentChain.length > 0}
49+ <p>
50+ Parent:
51+ {#each parentChain as parent, index}
52+ {#if index > 0}{' > '} {/if}
53+ <a use:link={parent.link}>{parent.patp}</a>
54+ {/each}
55+ </p>
56+ {/if}
57+ </div>
58+</div>
ui/src/components/ShipViewPage.sveltedeleted−6
@@ -1,6 +0,0 @@
1−<script lang="ts">
2− import ShipView from './ShipView.svelte';
3− export let params = {};
4−</script>
5−
6−<ShipView patp={params.patp} />
ui/src/components/Sigil.sveltenew file+36
@@ -0,0 +1,36 @@
1+<script lang="ts">
2+ import { clan } from 'urbit-ob';
3+ import { sigil, stringRenderer } from '@tlon/sigil-js';
4+ import moonPng from '../assets/moon.png';
5+ import cometSvg from '../assets/comet.svg';
6+
7+ export let patp: string;
8+ export let size: number;
9+ let svgString: any;
10+ let imgSrc: any;
11+
12+ $: shipClass = clan(patp);
13+ $: displaySigil = ['galaxy', 'star', 'planet'].includes(shipClass);
14+ $: {
15+ if (displaySigil) {
16+ svgString = sigil({
17+ patp: patp,
18+ renderer: stringRenderer,
19+ size,
20+ colors: ['black', 'white'],
21+ });
22+ } else if (shipClass === 'moon') {
23+ imgSrc = moonPng;
24+ } else {
25+ imgSrc = cometSvg;
26+ }
27+ }
28+</script>
29+
30+<span style="height: {size}px; width: {size}px;" >
31+ {#if displaySigil}
32+ {@html svgString}
33+ {:else}
34+ <img src={imgSrc} alt={shipClass} style="height: {size}px; width: {size}px;" />
35+ {/if}
36+</span>
ui/src/components/ValidationProblems.sveltenew file+19
@@ -0,0 +1,19 @@
1+<script lang="ts">
2+ export let problems: Array<string> = [];
3+</script>
4+
5+<div class="p-4 max-w-md rounded-lg bg-white">
6+ <p>
7+ Sorry, the ID you entered is invalid:
8+ </p>
9+ {#each problems as problem}
10+ <p>
11+ {problem}
12+ </p>
13+ {/each}
14+ <p>
15+ <!-- TODO: add link to doc -->
16+ You can read more here.
17+ </p>
18+</div>
19+
ui/src/fake_data/fake-contacts.tsnew file+92
@@ -0,0 +1,92 @@
1+export default {
2+ "~bus": {
3+ "status": "busbusbus",
4+ "last-updated": 1614902970000,
5+ "avatar": null,
6+ "cover": null,
7+ "bio": "Henlo",
8+ "nickname": "The Bus",
9+ "color": "0xff.cb13",
10+ "groups": [
11+
12+ ]
13+ },
14+ "~batbus": {
15+ "status": "batbusbatbusbatbus",
16+ "last-updated": 1614902970000,
17+ "avatar": null,
18+ "cover": null,
19+ "bio": "Henlo!!!!",
20+ "nickname": "The BatBus",
21+ "color": "0xff.cb13",
22+ "groups": [
23+
24+ ]
25+ },
26+ '~midlev-mindyr': {
27+ status: "not great, but not terrible",
28+ 'last-updated': 1614902973264,
29+ avatar : null,
30+ cover : null,
31+ bio : "I made this for you",
32+ nickname : "The Grantee",
33+ color : "0xff.cb13",
34+ groups: []
35+ },
36+ "~norsyr-torryn": {
37+ "status": "",
38+ "last-updated": 1633021688262,
39+ "avatar": null,
40+ "cover": null,
41+ "bio": "",
42+ "nickname": "The Champion",
43+ "color": "0x0",
44+ "groups": [
45+
46+ ]
47+ },
48+ '~midlev-minder': {
49+ status: "not great, but not terrible",
50+ 'last-updated': 1614902973264,
51+ avatar : null,
52+ cover : null,
53+ bio : "I made this for you",
54+ nickname : "The Doppelgänger",
55+ color : "0xff.cb13",
56+ groups: []
57+ },
58+ '~poster-hoster-midlev-mindyr': {
59+ status: "not great, but not terribly original",
60+ 'last-updated': 1614902973264,
61+ avatar : null,
62+ cover : null,
63+ bio : "I made this for you",
64+ nickname : "John Hyde's group hoster",
65+ color : "0xff.cb13",
66+ groups: []
67+ },
68+ "~pacred-sorfun-tobfet-padlet--bilpub-norrus-nosrux-litbec": {
69+ "status": "",
70+ "last-updated": 1633021688262,
71+ "avatar": null,
72+ "cover": null,
73+ "bio": "",
74+ "nickname": "Mr Comet",
75+ "color": "0x0",
76+ "groups": [
77+
78+ ]
79+ },
80+ "~zod": {
81+ "status": "",
82+ "last-updated": 1633021688262,
83+ "avatar": null,
84+ "cover": null,
85+ "bio": "",
86+ "nickname": "the zod",
87+ "color": "0x0",
88+ "groups": [
89+
90+ ]
91+ },
92+};
ui/src/lib/id.ts+102−20
@@ -1,11 +1,20 @@
11const prefixes = ['doz', 'mar', 'bin', 'wan', 'sam', 'lit', 'sig', 'hid', 'fid', 'lis', 'sog', 'dir', 'wac', 'sab', 'wis', 'sib', 'rig', 'sol', 'dop', 'mod', 'fog', 'lid', 'hop', 'dar', 'dor', 'lor', 'hod', 'fol', 'rin', 'tog', 'sil', 'mir', 'hol', 'pas', 'lac', 'rov', 'liv', 'dal', 'sat', 'lib', 'tab', 'han', 'tic', 'pid', 'tor', 'bol', 'fos', 'dot', 'los', 'dil', 'for', 'pil', 'ram', 'tir', 'win', 'tad', 'bic', 'dif', 'roc', 'wid', 'bis', 'das', 'mid', 'lop', 'ril', 'nar', 'dap', 'mol', 'san', 'loc', 'nov', 'sit', 'nid', 'tip', 'sic', 'rop', 'wit', 'nat', 'pan', 'min', 'rit', 'pod', 'mot', 'tam', 'tol', 'sav', 'pos', 'nap', 'nop', 'som', 'fin', 'fon', 'ban', 'mor', 'wor', 'sip', 'ron', 'nor', 'bot', 'wic', 'soc', 'wat', 'dol', 'mag', 'pic', 'dav', 'bid', 'bal', 'tim', 'tas', 'mal', 'lig', 'siv', 'tag', 'pad', 'sal', 'div', 'dac', 'tan', 'sid', 'fab', 'tar', 'mon', 'ran', 'nis', 'wol', 'mis', 'pal', 'las', 'dis', 'map', 'rab', 'tob', 'rol', 'lat', 'lon', 'nod', 'nav', 'fig', 'nom', 'nib', 'pag', 'sop', 'ral', 'bil', 'had', 'doc', 'rid', 'moc', 'pac', 'rav', 'rip', 'fal', 'tod', 'til', 'tin', 'hap', 'mic', 'fan', 'pat', 'tac', 'lab', 'mog', 'sim', 'son', 'pin', 'lom', 'ric', 'tap', 'fir', 'has', 'bos', 'bat', 'poc', 'hac', 'tid', 'hav', 'sap', 'lin', 'dib', 'hos', 'dab', 'bit', 'bar', 'rac', 'par', 'lod', 'dos', 'bor', 'toc', 'hil', 'mac', 'tom', 'dig', 'fil', 'fas', 'mit', 'hob', 'har', 'mig', 'hin', 'rad', 'mas', 'hal', 'rag', 'lag', 'fad', 'top', 'mop', 'hab', 'nil', 'nos', 'mil', 'fop', 'fam', 'dat', 'nol', 'din', 'hat', 'nac', 'ris', 'fot', 'rib', 'hoc', 'nim', 'lar', 'fit', 'wal', 'rap', 'sar', 'nal', 'mos', 'lan', 'don', 'dan', 'lad', 'dov', 'riv', 'bac', 'pol', 'lap', 'tal', 'pit', 'nam', 'bon', 'ros', 'ton', 'fod', 'pon', 'sov', 'noc', 'sor', 'lav', 'mat', 'mip', 'fip'];
22const suffixes = ['zod', 'nec', 'bud', 'wes', 'sev', 'per', 'sut', 'let', 'ful', 'pen', 'syt', 'dur', 'wep', 'ser', 'wyl', 'sun', 'ryp', 'syx', 'dyr', 'nup', 'heb', 'peg', 'lup', 'dep', 'dys', 'put', 'lug', 'hec', 'ryt', 'tyv', 'syd', 'nex', 'lun', 'mep', 'lut', 'sep', 'pes', 'del', 'sul', 'ped', 'tem', 'led', 'tul', 'met', 'wen', 'byn', 'hex', 'feb', 'pyl', 'dul', 'het', 'mev', 'rut', 'tyl', 'wyd', 'tep', 'bes', 'dex', 'sef', 'wyc', 'bur', 'der', 'nep', 'pur', 'rys', 'reb', 'den', 'nut', 'sub', 'pet', 'rul', 'syn', 'reg', 'tyd', 'sup', 'sem', 'wyn', 'rec', 'meg', 'net', 'sec', 'mul', 'nym', 'tev', 'web', 'sum', 'mut', 'nyx', 'rex', 'teb', 'fus', 'hep', 'ben', 'mus', 'wyx', 'sym', 'sel', 'ruc', 'dec', 'wex', 'syr', 'wet', 'dyl', 'myn', 'mes', 'det', 'bet', 'bel', 'tux', 'tug', 'myr', 'pel', 'syp', 'ter', 'meb', 'set', 'dut', 'deg', 'tex', 'sur', 'fel', 'tud', 'nux', 'rux', 'ren', 'wyt', 'nub', 'med', 'lyt', 'dus', 'neb', 'rum', 'tyn', 'seg', 'lyx', 'pun', 'res', 'red', 'fun', 'rev', 'ref', 'mec', 'ted', 'rus', 'bex', 'leb', 'dux', 'ryn', 'num', 'pyx', 'ryg', 'ryx', 'fep', 'tyr', 'tus', 'tyc', 'leg', 'nem', 'fer', 'mer', 'ten', 'lus', 'nus', 'syl', 'tec', 'mex', 'pub', 'rym', 'tuc', 'fyl', 'lep', 'deb', 'ber', 'mug', 'hut', 'tun', 'byl', 'sud', 'pem', 'dev', 'lur', 'def', 'bus', 'bep', 'run', 'mel', 'pex', 'dyt', 'byt', 'typ', 'lev', 'myl', 'wed', 'duc', 'fur', 'fex', 'nul', 'luc', 'len', 'ner', 'lex', 'rup', 'ned', 'lec', 'ryd', 'lyd', 'fen', 'wel', 'nyd', 'hus', 'rel', 'rud', 'nes', 'hes', 'fet', 'des', 'ret', 'dun', 'ler', 'nyr', 'seb', 'hul', 'ryl', 'lud', 'rem', 'lys', 'fyn', 'wer', 'ryc', 'sug', 'nys', 'nyl', 'lyn', 'dyn', 'dem', 'lux', 'fed', 'sed', 'bec', 'mun', 'lyr', 'tes', 'mud', 'nyt', 'byr', 'sen', 'weg', 'fyr', 'mur', 'tel', 'rep', 'teg', 'pec', 'nel', 'nev', 'fes'];
3−const illegalCharsRegex = /[^-_*^abcdefghilmnoprstuvwxyz]/g;
3+const illegalIdCharsRegex = /[^-_^~abcdefghilmnoprstuvwxyz]/g;
4+const illegalSearchCharsRegex = /[^-_*^~abcdefghilmnoprstuvwxyz]/g;
5+
6+const wildWord = '******';
7+const moonFiller = `${wildWord}-${wildWord}`;
8+const cometFiller = `-${wildWord}-${wildWord}-${wildWord}--${wildWord}-${wildWord}-${wildWord}-`;
49
510// Error Messages
6−const emptyPatpMessage = "ID cannot be empty."
7−const emptyWordsMessage = "Separators (- ^ _) cannot appear next to each other, or at the beginning or end of an ID."
8−const invalidAbbreviations = "Moon and Comet abbreviations (using ^ or _) can only have two parts."
11+const emptyPatpMessage = "ID cannot be empty.";
12+const improperSigMessage = "There should only be one ~ in an ID, at the beginning.";
13+const emptyWordsMessage = "Separators (- ^ _) cannot appear next to each other, except for a '--' before the last four parts of a comet ID.";
14+const separatorsNeighboringMessage = "Separators (- ^ _) cannot appear next to each other, except for a '--' before the last four parts of a comet ID.";
15+const outsideSeparatorsMessage = "Separators (- ^ _) cannot appear at the beginning or end of an ID.";
16+const invalidCometIdMessage = "Comet IDs should be 16 syllables, in groups of two, separated by '-', and by '--' in the very middle.";;
17+const tooLongAbbreviationMessage = "Moon and Comet abbreviations (using ^ or _) can only have two parts.";
918
1019function illegalCharsMessage(illegalChars: string[]) {
1120 return 'It contains illegal characters: ' + listStrings(illegalChars);
@@ -15,11 +24,11 @@ function multipleWordsLengthMessage(wrongLengthWords: string[]) {
1524 let wordListString = listStrings(wrongLengthWords, true);
1625 let firstHalf = '';
1726 if (wrongLengthWords.length > 1) {
18− firstHalf = `${wordListString} are not valid parts of a multi-part ID.`;
27+ firstHalf = `${wordListString} are the wrong lengths.`;
1928 } else {
20− firstHalf = `${wordListString} is not a valid part of a multi-part ID.`;
29+ firstHalf = `${wordListString} is the wrong length.`;
2130 }
22− return `${firstHalf} Each part must be 6 letters long.`;
31+ return `${firstHalf} Each part of a planet, moon, or comet ID must be 6 letters long.`;
2332}
2433
2534function singleWordLengthMessage(word: string) {
@@ -39,13 +48,32 @@ function invalidSuffixesMessage(syllables: string[]) {
3948 return invalidSyllablesMessage(syllables, 'suffixes');
4049}
4150
42−function stripTilde(str: string): string {
51+function sig(str: string): string {
52+ if (str[0] !== '~') {
53+ return '~' + str;
54+ }
55+ return str;
56+}
57+
58+function desig(str: string): string {
4359 if (str[0] === '~') {
4460 return str.substring(1);
4561 }
4662 return str;
4763}
4864
65+function normalizeId(patp: string): string {
66+ patp = patp.toLowerCase();
67+ patp = sig(patp);
68+ return patp;
69+}
70+
71+function normalizeIdAndDesig(patp: string): string {
72+ patp = patp.toLowerCase();
73+ patp = desig(patp);
74+ return patp;
75+}
76+
4977function listStrings(list: string[], includeAnd: boolean = false): string {
5078 if (includeAnd && list.length > 1) {
5179 return listStrings(list.slice(0, -1)) + ' and ' + listStrings(list.slice(-1));
@@ -54,17 +82,17 @@ function listStrings(list: string[], includeAnd: boolean = false): string {
5482}
5583
5684function splitIdIntoWords(str: string): string[] {
57− return str.split(/[-_^]/);
85+ return str.split(/(?:--|[-_^])/);
5886}
5987
60−function convertSyllableToRegex(str: string): RegExp {
61− str = str.replace(/\*/g, '.');
88+function convertSearchTextToRegex(str: string): RegExp {
89+ str = '^' + str.replace(/\*/g, '.') + '$';
6290 return new RegExp(str);
6391}
6492
6593function syllableSearchValid(search: string, syllables: string[]) {
6694 for (let syllable of syllables) {
67− let regex = convertSyllableToRegex(search);
95+ let regex = convertSearchTextToRegex(search);
6896 if (regex.test(syllable)) {
6997 return true;
7098 }
@@ -80,10 +108,14 @@ function suffixSearchValid(search: string) {
80108 return syllableSearchValid(search, suffixes);
81109}
82110
83−export function validateId(patp: string): string[] {
111+function validateIdSearchText(patp: string): string[] {
112+ return validateId(patp, true);
113+}
114+
115+
116+function validateId(patp: string, isSearchQuery: boolean = false): string[] {
84117 let problems = [];
85− patp = patp.toLowerCase();
86− patp = stripTilde(patp);
118+ patp = normalizeIdAndDesig(patp);
87119
88120 // Validate patp is not empty
89121 if (patp.length === 0) {
@@ -91,12 +123,24 @@ export function validateId(patp: string): string[] {
91123 return problems;
92124 }
93125
126+ // Validate proper use of sig
127+ if (/~/.test(patp)) {
128+ problems.push(improperSigMessage);
129+ }
130+
94131 // Validate all characters are legal
132+ const illegalCharsRegex = isSearchQuery ? illegalSearchCharsRegex : illegalIdCharsRegex;
95133 let illegalChars = patp.match(illegalCharsRegex);
96134 if (illegalChars) {
97135 problems.push(illegalCharsMessage(illegalChars));
98136 }
99137
138+ // Validate proper use of separators
139+ if (/^([-^_].*)?(.*[-^_])?$/.test(patp)) {
140+ problems.push(outsideSeparatorsMessage);
141+ patp = patp.match(/^[-^_]*(.*?)[-^_]*$/)[1];
142+ }
143+
100144 // Validate word lengths
101145 let words = splitIdIntoWords(patp);
102146 if (words.length > 1) {
@@ -124,11 +168,26 @@ export function validateId(patp: string): string[] {
124168 }
125169 }
126170
127− // Validate proper use of separators
128− if (words.length > 2) {
129− let manyPartIdRegex = /^(\w+-)+\w+$/;
171+ // Don't bother checking syllables if there are already problems
172+ if (problems.length > 0) {
173+ return problems;
174+ }
175+
176+ // Validate remaining structural edge cases
177+ if (words.length >= 5) {
178+ let cometIdRegex = /^([\w*]+-)+-([\w*]+-){3}[\w*]+$/;
179+ if (!cometIdRegex.test(patp)) {
180+ problems.push(invalidCometIdMessage);
181+ }
182+ } else if (words.length > 2) {
183+ let manyPartIdRegex = /^([\w*]+-)+[\w*]+$/;
130184 if (!manyPartIdRegex.test(patp)) {
131− problems.push(invalidAbbreviations);
185+ problems.push(tooLongAbbreviationMessage);
186+ }
187+ } else if (words.length == 2) {
188+ let twoPartIdRegex = /^[\w*]+[-^_][\w*]+$/;
189+ if (!twoPartIdRegex.test(patp)) {
190+ problems.push(tooLongAbbreviationMessage);
132191 }
133192 }
134193
@@ -163,4 +222,27 @@ export function validateId(patp: string): string[] {
163222 }
164223
165224 return problems;
166−}
225+}
226+
227+function expandAbbreviatedId(patp: string): string {
228+ patp = normalizeIdAndDesig(patp);
229+ if (/\^/.test(patp)) {
230+ patp = moonFiller + '-' + patp.replace(/\^/, '-');
231+ } else if (/_/.test(patp)) {
232+ patp = patp.replace('_', cometFiller);
233+ }
234+ return patp;
235+}
236+
237+function convertIdSearchTextToQuery(patp: string): RegExp {
238+ return convertSearchTextToRegex(sig(expandAbbreviatedId(patp)));
239+}
240+
241+export {
242+ normalizeId,
243+ normalizeIdAndDesig,
244+ validateIdSearchText,
245+ validateId,
246+ convertIdSearchTextToQuery,
247+ convertSearchTextToRegex,
248+};
ui/src/lib/link.tsnew file+3
@@ -0,0 +1,3 @@
1+export function linkToShip(patp: string): string {
2+ return `/ship/${patp}`;
3+}
ui/src/lib/utils.tsnew file+7
@@ -0,0 +1,7 @@
1+function filterObject(obj, predicate) {
2+ return Object.fromEntries(Object.entries(obj).filter(([key, value]) => predicate(key, value)));
3+}
4+
5+export {
6+ filterObject,
7+}
ui/src/routes/Home.sveltenew file+11
@@ -0,0 +1,11 @@
1+<script lang="ts">
2+ import SearchBar from "../components/SearchBar.svelte";
3+ import SearchResults from "../components/SearchResults.svelte";
4+ let searchText: string;
5+ let searchQuery: RegExp;
6+</script>
7+
8+<div class="m-8 flex flex-col space-y-8">
9+ <SearchBar bind:searchText bind:searchQuery />
10+ <SearchResults bind:searchText {searchQuery}/>
11+</div>
ui/src/routes/ShipViewPage.sveltenew file+26
@@ -0,0 +1,26 @@
1+<script lang="ts">
2+ import { link, replace } from 'svelte-spa-router'
3+ import { validateId, normalizeId } from '../lib/id';
4+ import { linkToShip } from '../lib/link';
5+ import ShipView from '../components/ShipView.svelte';
6+ import ValidationErrors from '../components/ValidationProblems.svelte';
7+ export let params: any = {};
8+
9+ $: patp = normalizeId(params.patp);
10+ $: problems = validateId(patp);
11+ $: patpIsValid = problems.length === 0;
12+ $: {
13+ if (patpIsValid && patp !== params.patp) {
14+ replace(linkToShip(patp));
15+ }
16+ }
17+</script>
18+
19+<div class="m-8 flex flex-col space-y-8">
20+ <a use:link={"/"} class="p-4 bg-white rounded-lg text-center">Go Home</a>
21+ {#if patpIsValid}
22+ <ShipView patp={patp} />
23+ {:else}
24+ <ValidationErrors {problems} />
25+ {/if}
26+</div>
ui/src/stores/store.tsnew file+40
@@ -0,0 +1,40 @@
1+import { writable } from 'svelte/store';
2+import type { StoreState } from '../types/store';
3+import { filterObject } from '../lib/utils';
4+import fakeContacts from '../fake_data/fake-contacts';
5+
6+const initStore: StoreState = {
7+ // chats: [],
8+ // groups: {},
9+ contacts: {},
10+ connection: 'disconnected',
11+ ship: window.ship,
12+};
13+
14+const { subscribe, update, set } = writable(initStore);
15+
16+function search(idQuery: RegExp) {
17+ update(($store: StoreState): StoreState => {
18+ if (!idQuery) {
19+ return {
20+ ...$store,
21+ contacts: fakeContacts,
22+ };
23+ }
24+ // const filteredContacts = filterObject($store.contacts, (id, contact) => {
25+ const filteredContacts = filterObject(fakeContacts, (id) => {
26+ return idQuery.test(id);
27+ });
28+ return {
29+ ...$store,
30+ contacts: filteredContacts,
31+ }
32+ });
33+}
34+
35+function reset() {
36+ set(initStore);
37+}
38+
39+
40+export default { subscribe, search, reset };
ui/src/styles/tailwind-output.css+107−35
@@ -424,8 +424,14 @@ Ensure the default browser behavior of the `hidden` attribute.
424424 --tw-backdrop-saturate: ;
425425 --tw-backdrop-sepia: ;
426426}
427−.m-4 {
428− margin: 1rem;
427+.absolute {
428+ position: absolute;
429+}
430+.relative {
431+ position: relative;
432+}
433+.right-0 {
434+ right: 0px;
429435}
430436.m-8 {
431437 margin: 2rem;
@@ -434,23 +440,51 @@ Ensure the default browser behavior of the `hidden` attribute.
434440 margin-left: 2rem;
435441 margin-right: 2rem;
436442}
443+.mx-auto {
444+ margin-left: auto;
445+ margin-right: auto;
446+}
447+.mt-0 {
448+ margin-top: 0px;
449+}
450+.mb-8 {
451+ margin-bottom: 2rem;
452+}
453+.-mt-2 {
454+ margin-top: -0.5rem;
455+}
456+.mr-4 {
457+ margin-right: 1rem;
458+}
459+.ml-4 {
460+ margin-left: 1rem;
461+}
437462.flex {
438463 display: flex;
439464}
440−.h-full {
441− height: 100%;
465+.grid {
466+ display: grid;
467+}
468+.h-10 {
469+ height: 2.5rem;
470+}
471+.h-6 {
472+ height: 1.5rem;
473+}
474+.h-8 {
475+ height: 2rem;
442476}
443−.h-screen {
444− height: 100vh;
477+.h-12 {
478+ height: 3rem;
445479}
446480.w-full {
447481 width: 100%;
448482}
449−.w-auto {
450− width: auto;
483+.w-10 {
484+ width: 2.5rem;
451485}
452−.max-w-lg {
453− max-width: 32rem;
486+.w-12 {
487+ width: 3rem;
454488}
455489.max-w-md {
456490 max-width: 28rem;
@@ -458,54 +492,92 @@ Ensure the default browser behavior of the `hidden` attribute.
458492.grow {
459493 flex-grow: 1;
460494}
495+.grid-cols-3 {
496+ grid-template-columns: repeat(3, minmax(0, 1fr));
497+}
498+.flex-col {
499+ flex-direction: column;
500+}
501+.flex-wrap {
502+ flex-wrap: wrap;
503+}
504+.items-center {
505+ align-items: center;
506+}
461507.justify-center {
462508 justify-content: center;
463509}
464−.justify-items-center {
465− justify-items: center;
510+.space-y-8 > :not([hidden]) ~ :not([hidden]) {
511+ --tw-space-y-reverse: 0;
512+ margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
513+ margin-bottom: calc(2rem * var(--tw-space-y-reverse));
466514}
467−.rounded-lg {
468− border-radius: 0.5rem;
515+.justify-self-start {
516+ justify-self: start;
469517}
470−.rounded-xl {
471− border-radius: 0.75rem;
518+.justify-self-end {
519+ justify-self: end;
472520}
473−.rounded-2xl {
474− border-radius: 1rem;
521+.break-words {
522+ overflow-wrap: break-word;
523+}
524+.break-all {
525+ word-break: break-all;
475526}
476527.rounded-full {
477528 border-radius: 9999px;
478529}
479−.border {
480− border-width: 1px;
530+.rounded-lg {
531+ border-radius: 0.5rem;
532+}
533+.rounded {
534+ border-radius: 0.25rem;
535+}
536+.border-b {
537+ border-bottom-width: 1px;
481538}
482539.bg-black {
483540 --tw-bg-opacity: 1;
484541 background-color: rgb(0 0 0 / var(--tw-bg-opacity));
485542}
486−.bg-green-400 {
487− --tw-bg-opacity: 1;
488− background-color: rgb(74 222 128 / var(--tw-bg-opacity));
489−}
490−.bg-green-600 {
543+.bg-white {
491544 --tw-bg-opacity: 1;
492− background-color: rgb(22 163 74 / var(--tw-bg-opacity));
545+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
493546}
494547.p-4 {
495548 padding: 1rem;
496549}
497−.px-4 {
498− padding-left: 1rem;
499− padding-right: 1rem;
550+.p-6 {
551+ padding: 1.5rem;
552+}
553+.p-8 {
554+ padding: 2rem;
500555}
501556.py-2 {
502557 padding-top: 0.5rem;
503558 padding-bottom: 0.5rem;
504559}
505−.px-6 {
506− padding-left: 1.5rem;
507− padding-right: 1.5rem;
560+.text-center {
561+ text-align: center;
562+}
563+.text-2xl {
564+ font-size: 1.5rem;
565+ line-height: 2rem;
566+}
567+.opacity-10 {
568+ opacity: 0.1;
569+}
570+.filter {
571+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
572+}
573+.transition-opacity {
574+ transition-property: opacity;
575+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
576+ transition-duration: 150ms;
577+}
578+.last\:border-none:last-child {
579+ border-style: none;
580+}
581+.hover\:opacity-40:hover {
582+ opacity: 0.4;
508583}
509−.outline {
510− outline-style: solid;
511−}
ui/src/styles/tailwind.css+1−1
@@ -1,3 +1,3 @@
11@tailwind base;
22@tailwind components;
3−@tailwind utilities;
3+@tailwind utilities;
ui/src/types/connection.tsnew file+1
@@ -0,0 +1 @@
1+export type ConnectionStatus = 'reconnecting' | 'disconnected' | 'connected';
ui/src/types/store.tsnew file+16
@@ -0,0 +1,16 @@
1+import type { ConnectionStatus } from './connection';
2+import type { Patp, Group, Contact } from '@urbit/api';
3+
4+export interface StoreState {
5+ // Urbit
6+ // groups: {
7+ // [group: string]: Group;
8+ // };
9+ contacts: {
10+ [contact: string]: Contact;
11+ };
12+ // chats: [];
13+ connection: ConnectionStatus;
14+ subscription?: unknown;
15+ ship: Patp;
16+}
ui/svelte.config.js+7−1
@@ -3,5 +3,11 @@ import sveltePreprocess from 'svelte-preprocess'
33export default {
44 // Consult https://github.com/sveltejs/svelte-preprocess
55 // for more information about preprocessors
6− preprocess: sveltePreprocess()
6+ preprocess: sveltePreprocess(),
7+ onwarn: (warning, handler) => {
8+ // Don't warn when using use:link
9+ if (warning.code === 'a11y-missing-attribute') return
10+
11+ handler(warning)
12+ },
713}
ui/tailwind.config.cjs+4−5
@@ -1,13 +1,12 @@
11module.exports = {
22 mode: 'jit',
3− purge: ['./index.html', './src/**/*.{js,ts,jsx,tsx,svelte}'],
4− darkMode: 'media', // or 'media' or 'class'
3+ content: [
4+ './index.html',
5+ './src/**/*.{js,ts,jsx,tsx,svelte}',
6+ ],
57 theme: {
68 extend: {}
79 },
810 screens: {},
9− variants: {
10− extend: {}
11− },
1211 plugins: []
1312};
ui/tsconfig.json+1
@@ -1,6 +1,7 @@
11{
22 "extends": "@tsconfig/svelte/tsconfig.json",
33 "compilerOptions": {
4+ "moduleResolution": "node",
45 "target": "esnext",
56 "useDefineForClassFields": true,
67 "module": "esnext",