on main help sign in
fix bugs and add keybord shortcuts
0eeff2cJohn Hyde
desk/doc/usr/astrolabe.udon+13−3
@@ -9,6 +9,16 @@ spawned ships with partial or wildcard searches, e.g. `sampel-***net`.
9There's also Help Documentation if
9There's also Help Documentation if
10you want to learn more about how Urbit IDs work.
10you want to learn more about how Urbit IDs work.
11
11
12+## Keyboard Shortcuts
13+
14+- `/` - focus the search bar
15+- `escape` - leave text fields, close help pages, etc
16+- `?` - open the help docs
17+- `s` - switch to sigil search
18+- `p` - switch to patp search
19+- `backspace` or `x` - clear sigil input info
20+
21+
12## How to Use Search
22## How to Use Search
13
23
14When you type part of a `@p` into the search bar, all your contacts and peers will be
24When you type part of a `@p` into the search bar, all your contacts and peers will be
@@ -71,9 +81,9 @@ The Parent view simply shows each point under the point that spawned it. All spa
71are shown in this view, with the exception of locked stars.
81are shown in this view, with the exception of locked stars.
72In both views, all galaxies are shown for completeness.
82In both views, all galaxies are shown for completeness.
73
83
74−## Upcoming Features
84+The All/Peers/Pals selector lets you filter which points are easily visible in the graph.
75−
85+If you select Peers, only those ships that your ship has talked to will be full-colored.
76−In addition to improving the search features over time, Astrolabe will one day feature integrations with Groups, `%pals`, `%gora`, and whatever else seems fun and/or useful.
86+If you select Pals, only ships you've added in `%pals` will be full-colored.
77
87
78## Info
88## Info
79
89
ui/src/App.svelte+35−1
@@ -5,6 +5,8 @@
5 import Home from './routes/Home.svelte';
5 import Home from './routes/Home.svelte';
6 import HelpModal from '@/docs/HelpModal.svelte';
6 import HelpModal from '@/docs/HelpModal.svelte';
7 import "./styles/styles.scss";
7 import "./styles/styles.scss";
8+ import docs from 'stores/docs';
9+ import keys from 'stores/keys';
8
10
9 // const routes = {
11 // const routes = {
10 // '/help/:path?': HelpPage,
12 // '/help/:path?': HelpPage,
@@ -30,6 +32,36 @@
30 $: {
32 $: {
31 document.title = title
33 document.title = title
32 }
34 }
35+
36+ function inputEvent(event) {
37+ if (event.target.tagName == 'INPUT') return true;
38+ if (event.target.tagName == 'TEXTAREA') return true;
39+ return false;
40+ }
41+
42+ function handleKeyDown(event: KeyboardEvent) {
43+ keys.runCallbacks($keys.globalkeydown, event);
44+ if (!inputEvent(event)) {
45+ keys.runCallbacks($keys.keydown, event);
46+ }
47+ }
48+
49+ function handleKeyUp(event: KeyboardEvent) {
50+ keys.runCallbacks($keys.globalkeyup, event);
51+ if (!inputEvent(event)) {
52+ keys.runCallbacks($keys.keyup, event);
53+ }
54+ }
55+
56+ $:{
57+ $keys.keyup.get('?').set('app', () => docs.show(''));
58+ $keys.keyup.get('shift+?').set('app', () => docs.show(''));
59+ $keys.globalkeyup.get('escape').set('app', (event) => {
60+ if (inputEvent(event)) {
61+ event.target.blur();
62+ }
63+ });
64+ }
33</script>
65</script>
34
66
35<main class="h-full overflow-y-auto">
67<main class="h-full overflow-y-auto">
@@ -39,4 +71,6 @@
39
71
40<svelte:head>
72<svelte:head>
41 <title>{title}</title>
73 <title>{title}</title>
42−</svelte:head>
74+</svelte:head>
75+
76+<svelte:body on:keydown={handleKeyDown} on:keyup={handleKeyUp} />
ui/src/components/docs/Doc.svelte+10−1
@@ -1,9 +1,10 @@
1<script lang="ts">
1<script lang="ts">
2− import { createEventDispatcher } from 'svelte';
2+ import { createEventDispatcher, onDestroy } from 'svelte';
3 import DocContent from './DocContent.svelte';
3 import DocContent from './DocContent.svelte';
4 import DocHome from './DocHome.svelte';
4 import DocHome from './DocHome.svelte';
5 import Hamburger from '@/buttons/Hamburger.svelte';
5 import Hamburger from '@/buttons/Hamburger.svelte';
6 import CloseButton from '@/buttons/CloseButton.svelte';
6 import CloseButton from '@/buttons/CloseButton.svelte';
7+ import keys from 'stores/keys';
7
8
8 export let path: string;
9 export let path: string;
9
10
@@ -17,6 +18,14 @@
17 function navigate(path) {
18 function navigate(path) {
18 dispatch('navigate', path);
19 dispatch('navigate', path);
19 }
20 }
21+
22+ $: {
23+ $keys.globalkeyup.get('escape').set('doc', close);
24+ }
25+
26+ onDestroy(() => {
27+ $keys.globalkeyup.get('escape').delete('doc');
28+ });
20</script>
29</script>
21
30
22<div class="rounded-lg bg-white">
31<div class="rounded-lg bg-white">
ui/src/components/search/SearchBar.svelte+18−1
@@ -3,6 +3,7 @@
3 import Symbol from './sigil/Symbol.svelte';
3 import Symbol from './sigil/Symbol.svelte';
4
4
5 import docs from 'stores/docs';
5 import docs from 'stores/docs';
6+ import keys from 'stores/keys';
6 import { SearchAnalysis } from 'lib/id';
7 import { SearchAnalysis } from 'lib/id';
7 import { SigilQuery } from 'lib/sigil';
8 import { SigilQuery } from 'lib/sigil';
8
9
@@ -78,8 +79,22 @@
78 }
79 }
79 }
80 }
80
81
82+ let searchInput;
83+ let searchFocused = false;
84+ function onFocus() { searchFocused = true; }
85+ function onBlur() { searchFocused = false; }
86+ function focus() { if (searchInput) searchInput.focus() }
87+ function blur() { if (searchInput) searchInput.blur() }
88+ $:{
89+ $keys.keyup.get('/').set('searchBar', focus);
90+ // generic binding in App.svelte takes care of blur
91+ $keys.keyup.get('p').set('searchBar', closeSigilInput);
92+ $keys.keyup.get('s').set('searchBar', openSigilInput);
93+ }
94+
81 function openSigilInput() {
95 function openSigilInput() {
96+ if (allowSigilSearch)
82− searchMode = 'sigil';
97+ searchMode = 'sigil';
83 }
98 }
84
99
85 function closeSigilInput() {
100 function closeSigilInput() {
@@ -118,6 +133,8 @@
118 spellcheck="false"
133 spellcheck="false"
119 placeholder={placeholderText}
134 placeholder={placeholderText}
120 on:input={(e) => bouncySearch = e.target.value}
135 on:input={(e) => bouncySearch = e.target.value}
136+ on:focus={onFocus} on:blur={onBlur}
137+ bind:this={searchInput}
121 value={search}
138 value={search}
122 autofocus
139 autofocus
123 />
140 />
ui/src/components/search/sigil/ClanSelect.svelte+10−2
@@ -3,20 +3,28 @@
3
3
4 import { capitalize } from 'lib/utils';
4 import { capitalize } from 'lib/utils';
5 import { CLANS, Clan } from 'types/sigil';
5 import { CLANS, Clan } from 'types/sigil';
6+ import keys from 'stores/keys';
6
7
7 export let clan: Clan = 'planet';
8 export let clan: Clan = 'planet';
8
9
9 let dropdownOpen = false;
10 let dropdownOpen = false;
10
11
12+ function openDropdown() { dropdownOpen = true }
13+ function closeDropdown() { dropdownOpen = false }
14+
11 function selectClan(itemClan: typeof clan) {
15 function selectClan(itemClan: typeof clan) {
12 clan = itemClan;
16 clan = itemClan;
13 dropdownOpen = false;
17 dropdownOpen = false;
14 }
18 }
19+
20+ $: {
21+ $keys.keyup.get('escape').set('clanSelect', closeDropdown);
22+ }
15</script>
23</script>
16
24
17<div class="text-xl text-center bg-white">
25<div class="text-xl text-center bg-white">
18 <p
26 <p
19− on:click={() => dropdownOpen = true}
27+ on:click={openDropdown}
20 class=" py-2"
28 class=" py-2"
21 >
29 >
22 {capitalize(clan)}
30 {capitalize(clan)}
@@ -24,7 +32,7 @@
24 </p>
32 </p>
25 {#if dropdownOpen}
33 {#if dropdownOpen}
26 <div class="absolute z-20 top-1 w-full flex flex-col mx-auto divide-y bg-white rounded-2xl border-b">
34 <div class="absolute z-20 top-1 w-full flex flex-col mx-auto divide-y bg-white rounded-2xl border-b">
27− <h3 class="py-2" on:click={() => dropdownOpen = false}>
35+ <h3 class="py-2" on:click={closeDropdown}>
28 Select Sigil Type
36 Select Sigil Type
29 </h3>
37 </h3>
30 {#each CLANS as itemClan}
38 {#each CLANS as itemClan}
ui/src/components/search/sigil/SigilInput.svelte+13−1
@@ -6,8 +6,9 @@
6 import ToggleButton from '@/buttons/ToggleButton.svelte';
6 import ToggleButton from '@/buttons/ToggleButton.svelte';
7
7
8 import type { SigilQuery } from 'lib/sigil';
8 import type { SigilQuery } from 'lib/sigil';
9− import { searchSettings } from 'stores/searchStores';
10 import { toggleStoreKey } from 'lib/utils';
9 import { toggleStoreKey } from 'lib/utils';
10+ import { searchSettings } from 'stores/searchStores';
11+ import keys from 'stores/keys';
11
12
12 export let sigilQuery: SigilQuery;
13 export let sigilQuery: SigilQuery;
13 let focusedSymbolIndex: number = undefined;
14 let focusedSymbolIndex: number = undefined;
@@ -37,6 +38,17 @@
37 $: {
38 $: {
38 sigilQuery.allowFictional = $searchSettings.allowFictionalSigils;
39 sigilQuery.allowFictional = $searchSettings.allowFictionalSigils;
39 }
40 }
41+
42+ function keyboardClear() {
43+ if (!focusedSymbol) {
44+ clear();
45+ }
46+ }
47+
48+ $: {
49+ $keys.keyup.get('x').set('sigilInput', keyboardClear);
50+ $keys.keyup.get('backspace').set('sigilInput', keyboardClear);
51+ }
40</script>
52</script>
41
53
42<div>
54<div>
ui/src/components/search/sigil/SymbolInputModeSelector.svelte+7
@@ -5,6 +5,7 @@
5 import SymbolInputModeSelectorButton from './SymbolInputModeSelectorButton.svelte';
5 import SymbolInputModeSelectorButton from './SymbolInputModeSelectorButton.svelte';
6
6
7 import { searchSettings } from 'stores/searchStores';
7 import { searchSettings } from 'stores/searchStores';
8+ import keys from 'stores/keys';
8 import type { PartType, ModeDef, ModeDefs } from 'types/sigil';
9 import type { PartType, ModeDef, ModeDefs } from 'types/sigil';
9 import type { SymbolQuery } from 'lib/sigil';
10 import type { SymbolQuery } from 'lib/sigil';
10 import { MODE_DEFS } from 'types/sigil';
11 import { MODE_DEFS } from 'types/sigil';
@@ -130,6 +131,12 @@
130 symbolQuery = symbolQuery.clearExceptPartType('geon');
131 symbolQuery = symbolQuery.clearExceptPartType('geon');
131 }
132 }
132 }
133 }
134+
135+ $: {
136+ $keys.keyup.get('x').set('sigilMode', clear);
137+ $keys.keyup.get('backspace').set('sigilMode', clear);
138+ $keys.keyup.get('escape').set('sigilMode', popMode);
139+ }
133</script>
140</script>
134
141
135<style lang="scss">
142<style lang="scss">
ui/src/lib/link.ts+3−3
@@ -1,16 +1,16 @@
1import { buildQuerystring } from './utils';
1import { buildQuerystring } from './utils';
2
2
3export function linkToShip(patp: string, queryParams = {}): string {
3export function linkToShip(patp: string, queryParams = {}): string {
4− return `/ship/${patp}` + buildQuerystring(queryParams);
4+ return `/ship/${encodeURIComponent(patp)}` + buildQuerystring(queryParams);
5}
5}
6
6
7export function linkToSearch(search: string, queryParams = {}): string {
7export function linkToSearch(search: string, queryParams = {}): string {
8− return `/search/${search}` + buildQuerystring(queryParams);
8+ return `/search/${encodeURIComponent(search)}` + buildQuerystring(queryParams);
9}
9}
10
10
11export function linkToChart(patp: string): string {
11export function linkToChart(patp: string): string {
12 if (patp) {
12 if (patp) {
13− return `/chart/${patp}`;
13+ return `/chart/${encodeURIComponent(patp)}`;
14 }
14 }
15 return `/chart`;
15 return `/chart`;
16}
16}
ui/src/routes/Home.svelte+1−6
@@ -12,6 +12,7 @@
12 import ValidationProblems from '@/ValidationProblems.svelte';
12 import ValidationProblems from '@/ValidationProblems.svelte';
13 import HomeScreenTiles from '@/HomeScreenTiles.svelte';
13 import HomeScreenTiles from '@/HomeScreenTiles.svelte';
14 export let params: any = {};
14 export let params: any = {};
15+
15 let analysis: SearchAnalysis = new SearchAnalysis();
16 let analysis: SearchAnalysis = new SearchAnalysis();
16 let sigilQuery: SigilQuery = new SigilQuery('planet');
17 let sigilQuery: SigilQuery = new SigilQuery('planet');
17 let navigating = false;
18 let navigating = false;
@@ -34,21 +35,17 @@
34 urlSearchMode = getUrlSearchMode(searchParams);
35 urlSearchMode = getUrlSearchMode(searchParams);
35 urlChangedSinceShipQuery = true;
36 urlChangedSinceShipQuery = true;
36 navigating = false;
37 navigating = false;
37− console.log('url changed most recently!');
38 }
38 }
39 }
39 }
40 $: {
40 $: {
41 urlChangedSinceShipQuery = (search || true) && searchMode && false;
41 urlChangedSinceShipQuery = (search || true) && searchMode && false;
42 }
42 }
43 function autoNav() {
43 function autoNav() {
44− console.log('judgement day, mfers!');
45 if ((search !== urlSearch || searchMode !== urlSearchMode)) {
44 if ((search !== urlSearch || searchMode !== urlSearchMode)) {
46 if (urlChangedSinceShipQuery) {
45 if (urlChangedSinceShipQuery) {
47 search = urlSearch;
46 search = urlSearch;
48 searchMode = urlSearchMode;
47 searchMode = urlSearchMode;
49− console.log('url wins!');
50 } else {
48 } else {
51− console.log('search wins!');
52 navigating = true;
49 navigating = true;
53 // create a temp one because the real one doesn't always update in time!
50 // create a temp one because the real one doesn't always update in time!
54 let tempAnalysis = new SearchAnalysis(search);
51 let tempAnalysis = new SearchAnalysis(search);
@@ -56,11 +53,9 @@
56 navToChart(tempAnalysis.patpIsValid);
53 navToChart(tempAnalysis.patpIsValid);
57 } else {
54 } else {
58 if (tempAnalysis.patpIsValid) {
55 if (tempAnalysis.patpIsValid) {
59− console.log('going to ship view!', search, tempAnalysis.patp);
60 navToShipView(tempAnalysis.patp, true);
56 navToShipView(tempAnalysis.patp, true);
61 } else {
57 } else {
62 const fromShipMode = params.mode === 'ship';
58 const fromShipMode = params.mode === 'ship';
63− console.log('going to search view!');
64 navToSearch(fromShipMode);
59 navToSearch(fromShipMode);
65 }
60 }
66 }
61 }
ui/src/stores/keys.tsnew file+68
@@ -0,0 +1,68 @@
1+import { writable, get } from 'svelte/store';
2+
3+const docs = {};
4+
5+class MapWithDefault<K, V> extends Map<K, V> {
6+ default: Function;
7+
8+ get(key: K) {
9+ if (!this.has(key)) {
10+ this.set(key, this.default());
11+ }
12+ return super.get(key);
13+ }
14+
15+ constructor(defaultFunction: () => V) {
16+ super();
17+ this.default = defaultFunction;
18+ }
19+}
20+
21+type Bindings = MapWithDefault<string, MapWithDefault<string, Function>>;
22+interface KeysStore {
23+ keydown: Bindings;
24+ keyup: Bindings;
25+ globalkeydown: Bindings;
26+ globalkeyup: Bindings;
27+};
28+function newEmptyBindings(): Bindings {
29+ return new MapWithDefault(() => new MapWithDefault(() => Function));
30+}
31+const initStore: KeysStore = {
32+ keydown: newEmptyBindings(),
33+ keyup: newEmptyBindings(),
34+ globalkeydown: newEmptyBindings(),
35+ globalkeyup: newEmptyBindings(),
36+};
37+
38+const store = writable(initStore);
39+const { subscribe, update, set } = store;
40+
41+function runCallbacks(bindings: Bindings, event: KeyboardEvent) {
42+ let els = [];
43+ if (event.ctrlKey) els.push('ctrl');
44+ if (event.metaKey) els.push('cmd');
45+ if (event.altKey) els.push('alt');
46+ if (event.shiftKey) els.push('shift');
47+ els.push(event.key == ' ' ? 'Space' : event.key.toLowerCase());
48+ let combo = els.join('+');
49+ // console.log(event);
50+ let callbacks = bindings.get(combo);
51+ if (callbacks) {
52+ callbacks.forEach((callback) => {
53+ try {
54+ if (callback)
55+ callback(event);
56+ } catch (e) {
57+ console.error(e);
58+ }
59+ });
60+ }
61+}
62+
63+export default {
64+ subscribe,
65+ update,
66+ set,
67+ runCallbacks,
68+};
ui/src/styles/tailwind-output.css+4
@@ -1871,6 +1871,10 @@ Ensure the default browser behavior of the `hidden` attribute.
1871 --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1871 --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1872 box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1872 box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1873}
1873}
1874+.blur {
1875+ --tw-blur: blur(8px);
1876+ 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);
1877+}
1874.drop-shadow {
1878.drop-shadow {
1875 --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
1879 --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
1876 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);
1880 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);