on main help sign in
sigil entry very nearly "complete"
commit67ad836d268cc26383ce7cff82512e4fa6b7b4b3 authorJohn Hyde <johnhydefour@gmail.com> dateWed 17 Aug 2022, 16:12 (4 years ago) parentef5db9c
15 files changed+740−308
ui/src/actions/symbolPartClick.ts+13−4 ui/src/assets/up-arrow.svg+1 ui/src/assets/x.svg+1 ui/src/components/Sigil.svelte+2−2 ui/src/components/search/sigil/ClanSelect.svelte+1−1 ui/src/components/search/sigil/GeonSelector.svelte+14−13 ui/src/components/search/sigil/SigilInput.svelte+46−15 ui/src/components/search/sigil/Symbol.svelte+6−1 ui/src/components/search/sigil/SymbolInput.svelte+54−8 ui/src/components/search/sigil/SymbolInputModeSelector.svelte+40−144 ui/src/components/search/sigil/SymbolInputModeSelectorButton.svelte+24−32 ui/src/lib/sigil.ts+197−8 ui/src/styles/tailwind-output.css+207 ui/src/types/sigil.ts+132−80 ui/src/types/store.ts+2
ui/src/actions/symbolPartClick.ts+13−4
@@ -1,12 +1,17 @@
11const docLinkRegex = /\/~\/scry\/docs\/usr\/astrolabe\/([-~._\w\d]+).html/;
22
3+
34function listenOnSvgElements(node, onPathChange) {
45 const parts = node.querySelectorAll('path, circle, line');
56 for (let part of parts) {
67 if (part.getAttribute('dataisgeon') === 'true') continue;
7− part.addEventListener('click', () => {
8− onPathChange(part.getAttribute('part-key'));
9− });
8+ if (onPathChange) {
9+ part.onclick = () => {
10+ onPathChange(part.getAttribute('part-key'));
11+ };
12+ } else {
13+ part.onclick = null;
14+ }
1015 }
1116}
1217
@@ -22,7 +27,11 @@ export default function svgClick(node: HTMLElement, { onPartClick, enabled = tru
2227
2328 return {
2429 update: ({ onPartClick, enabled = true, svgString }) => {
25− if (enabled) listenOnSvgElements(node, onPartClick);
30+ if (enabled) {
31+ listenOnSvgElements(node, onPartClick);
32+ } else {
33+ listenOnSvgElements(node, undefined);
34+ }
2635 },
2736 };
2837}
ui/src/assets/up-arrow.svgnew file+1
@@ -0,0 +1 @@
1+<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g data-name="Layer 2"><g data-name="arrow-ios-upward"><rect width="24" height="24" transform="rotate(180 12 12)" opacity="0"/><path d="M18 15a1 1 0 0 1-.64-.23L12 10.29l-5.37 4.32a1 1 0 0 1-1.41-.15 1 1 0 0 1 .15-1.41l6-4.83a1 1 0 0 1 1.27 0l6 5a1 1 0 0 1 .13 1.41A1 1 0 0 1 18 15z"/></g></g></svg>
ui/src/assets/x.svgnew file+1
@@ -0,0 +1 @@
1+<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g data-name="Layer 2"><g data-name="close"><rect width="24" height="24" transform="rotate(180 12 12)" opacity="0"/><path d="M13.41 12l4.3-4.29a1 1 0 1 0-1.42-1.42L12 10.59l-4.29-4.3a1 1 0 0 0-1.42 1.42l4.3 4.29-4.3 4.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l4.29-4.3 4.29 4.3a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42z"/></g></g></svg>
ui/src/components/Sigil.svelte+2−2
@@ -3,8 +3,8 @@
33 import { sigil as sigil1, stringRenderer as sr1 } from '@tlon/sigil-js';
44 import { sigil as sigil2, stringRenderer as sr2 } from '@johnhyde/sigil-js';
55 import { sigilScalingFunction } from 'lib/sigil';
6− import moonPng from '../assets/moon.png';
7− import cometSvg from '../assets/comet.svg';
6+ import moonPng from 'assets/moon.png';
7+ import cometSvg from 'assets/comet.svg';
88
99 export let patp: string;
1010 export let size: number;
ui/src/components/search/sigil/ClanSelect.svelte+1−1
@@ -28,7 +28,7 @@
2828 <span class="font-mono">▼</span>
2929 </p>
3030 {#if dropdownOpen}
31− <div class="absolute z-10 top-1 w-full flex flex-col mx-auto divide-y bg-white rounded-2xl border-b">
31+ <div class="absolute z-20 top-1 w-full flex flex-col mx-auto divide-y bg-white rounded-2xl border-b">
3232 <h3 class="py-2" on:click={() => dropdownOpen = false}>
3333 Select Sigil Type
3434 </h3>
ui/src/components/search/sigil/GeonSelector.svelte+14−13
@@ -1,29 +1,30 @@
11<script lang="ts">
2− import Symbol from './Symbol.svelte';
2+ import SymbolInputModeSelectorButton from './SymbolInputModeSelectorButton.svelte';
33
4− import type { PartType, SymbolQuery } from 'types/sigil';
4+ import { searchSettings } from 'stores/searchStores';
5+ import type { SymbolQuery } from 'lib/sigil';
56 import { GEONS } from 'types/sigil';
7+ import { arePartsPlausible } from 'lib/sigil';
68
79 export let symbolQuery: SymbolQuery;
810 export let popMode: Function;
911
10− let size = 48;
11−
1212 function setGeon(geon) {
1313 symbolQuery.geon = geon;
1414 popMode();
1515 }
16+
17+ $: componentsSansGeon = symbolQuery.components.filter(c => c[0] !== 'g');
1618</script>
1719
18−<!-- <div on:click={() => inputMode = undefined}>
19− done
20−</div>
21−<div on:click={() => setGeon(undefined)}>
22− clear
23−</div> -->
2420{#each GEONS as geon}
25− <div style:width="{size}px" on:click={() => setGeon(geon)}>
26− <Symbol components={[geon]} {size} />
27− </div>
21+ {@const plausible = $searchSettings.allowFictionalSigils ||
22+ arePartsPlausible(componentsSansGeon, symbolQuery.symbolType, [geon])
23+ }
24+ <SymbolInputModeSelectorButton components={[geon]}
25+ focused={geon === symbolQuery.geon}
26+ enabled={plausible}
27+ on:click={() => setGeon(geon)}
28+ />
2829{/each}
2930
ui/src/components/search/sigil/SigilInput.svelte+46−15
@@ -2,19 +2,25 @@
22 import SymbolInputModeSelector from './SymbolInputModeSelector.svelte';
33 import SymbolInput from './SymbolInput.svelte';
44 import ClanSelect from './ClanSelect.svelte';
5+ import ToggleButton from '@/buttons/ToggleButton.svelte';
56
6− import type { PartType } from 'types/sigil';
77 import type { SigilQuery } from 'lib/sigil';
8+ import { searchSettings } from 'stores/searchStores';
9+ import { toggleStoreKey } from 'lib/utils';
10+import { sigil } from '@tlon/sigil-js';
811
912 export let sigilQuery: SigilQuery;
1013 // export let clan: ('galaxy' | 'star' | 'planet') = 'planet';
11− let focusedSymbolIndex: number = 0;
14+ let focusedSymbolIndex: number = undefined;
1215 let inputComponents: string[] = [];
1316
1417 // let symbols: SymbolQuery = [];
1518
16− $: gridClasses = (sigilQuery.clan === 'galaxy' ? 'w-[50%]' : 'grid grid-cols-2 items-center');
19+ $: gridClasses = (sigilQuery.clan === 'galaxy' ? 'max-w-[10rem] grid items-center' : 'grid grid-cols-2 items-center');
1720
21+ function unfocusSymbol() {
22+ focusedSymbolIndex = undefined;
23+ }
1824 $: focusedSymbol = sigilQuery.activeSymbols[focusedSymbolIndex];
1925 $: {
2026 const numActiveSymbols = sigilQuery.activeSymbols.length;
@@ -22,36 +28,61 @@
2228 focusedSymbolIndex = numActiveSymbols - 1;
2329 }
2430 }
25− // function updateSigilQuery() {
26− // sigilQuery.setSymbol(focusedSymbol, focusedSymbolIndex);
27− // }
28− // $: focusedSymbol, updateSigilQuery();
31+
32+ function toggleAllowFictional() {
33+ toggleStoreKey(searchSettings, 'allowFictionalSigils');
34+ }
35+
36+ $: {
37+ sigilQuery.allowFictional = $searchSettings.allowFictionalSigils;
38+ }
2939</script>
3040
3141<div class="">
3242 <ClanSelect bind:clan={sigilQuery.clan} />
3343 <div class="flex flex-col xs:flex-row">
34− <div class="grow-[2] mx-auto w-full xs:m-0 xs:max-w-[18rem]">
35− <div class={`max-w-[18rem] mx-auto p-4 ${gridClasses} xs:rounded-bl-2xl`}>
44+ <div class="grow-[2] w-full xs:m-0 xs:max-w-[18rem]" on:click={unfocusSymbol}>
45+ <div class="max-w-[18rem] min-h-[14rem] mx-auto p-4 {gridClasses} xs:rounded-bl-2xl">
3646 {#each sigilQuery.activeSymbols as symbol, index}
3747 <SymbolInput
3848 bind:symbolQuery={symbol}
39− focused={index === focusedSymbolIndex}
49+ {focusedSymbolIndex}
50+ {index}
51+ clan={sigilQuery.clan}
4052 {inputComponents}
4153 on:click={() => focusedSymbolIndex = index}
4254 />
4355 {/each}
4456 </div>
4557 </div>
46− {#if focusedSymbol}
47− <div class="bg-green-100 grow rounded-b-2xl xs:rounded-bl-none">
58+ <div class="p-2 border-t grow rounded-b-2xl xs:border-l xs:border-t-0 xs:rounded-bl-none">
59+ {#if focusedSymbol}
4860 <SymbolInputModeSelector
4961 bind:symbolQuery={focusedSymbol}
5062 bind:inputComponents
51− unfocusSymbol={() => focusedSymbolIndex = undefined}
63+ unfocusSymbol={unfocusSymbol}
5264 />
53− </div>
54− {/if}
65+ {:else}
66+ Allow Sigils which don't exist:
67+ <ToggleButton on:click={toggleAllowFictional} on={$searchSettings.allowFictionalSigils} />
68+ {/if}
69+ </div>
5570 </div>
71+ {#each sigilQuery.activeSymbols as symbol}
72+ {@const plausibleSyllables = symbol.plausibleSyllables}
73+ {@const plausibleParts = symbol.plausibleParts}
74+ <p>
75+ {#if plausibleSyllables.length < 20 && plausibleSyllables.length > 0}
76+ {plausibleSyllables.join(', ')}:
77+ {#if plausibleParts.length < 20 && plausibleParts.length > 0}
78+ {plausibleParts.join(', ')}
79+ {:else}
80+ {plausibleParts.length}
81+ {/if}
82+ {:else}
83+ {plausibleSyllables.length}
84+ {/if}
85+ </p>
86+ {/each}
5687 <!-- {sigilQuery.string} -->
5788</div>
ui/src/components/search/sigil/Symbol.svelte+6−1
@@ -10,6 +10,7 @@
1010 export let bgColor: string = 'white';
1111 export let altBgColor: string = undefined;
1212 export let hoverColor: string = 'blue';
13+ export let opacity: number = 1;
1314 export let inverted: boolean = false;
1415 export let interactive: boolean = false;
1516 export let strokeWidthFactor: number = 1;
@@ -62,11 +63,15 @@
6263 stroke: var(--hoverColor);
6364 }
6465 }
66+
67+ circle:not([dataisgeon="true"]):not([fill="none"]):hover {
68+ fill: var(--hoverColor);
69+ }
6570 }
6671</style>
6772
6873<div class="aspect-square" class:interactive style:max-width="{size}px"
69− style="--hoverColor: {hoverColor};"
74+ style="--hoverColor: {hoverColor}; opacity: {opacity};"
7075 on:click
7176 use:symbolPartClick={{ onPartClick, enabled: interactive, svgString }}
7277>
ui/src/components/search/sigil/SymbolInput.svelte+54−8
@@ -2,15 +2,45 @@
22 import Symbol from './Symbol.svelte';
33 import without from 'lodash/without';
44
5− import type { PartType, SymbolQuery } from 'types/sigil';
5+ import { searchSettings } from 'stores/searchStores';
6+ import type { PartType } from 'types/sigil';
7+ import type { SymbolQuery } from 'lib/sigil';
8+ import { filterPartsByGeon } from 'lib/sigil';
69
710 export let symbolQuery: SymbolQuery;
811 // export let inputMode: PartType;
9− export let focused: boolean = false;
12+ export let focusedSymbolIndex: number;
13+ export let index: number = 0;
14+ export let clan: string = 'galaxy';
1015 export let inputComponents: string[] = [];
1116 let size = 128;
1217
13− $: focusedClasses = focused ? 'border border-gold-s1 border-2' : '';
18+ let translateClass;
19+ $: {
20+ if (focusedSymbolIndex === undefined) {
21+ translateClass = 'translate-x-0 translate-y-0';
22+ } else if (clan === 'star') {
23+ translateClass = index ? 'origin-right' : 'origin-left';
24+ } else if (clan === 'planet') {
25+ let cornerIndex = index;
26+ if (!focused) cornerIndex = focusedSymbolIndex;
27+ switch (cornerIndex) {
28+ case 0: translateClass = 'translate-x-1/4 translate-y-1/4'; break;
29+ case 1: translateClass = '-translate-x-1/4 translate-y-1/4'; break;
30+ case 2: translateClass = 'translate-x-1/4 -translate-y-1/4'; break;
31+ case 3:
32+ default: translateClass = '-translate-x-1/4 -translate-y-1/4';
33+ }
34+ }
35+ }
36+ $: scaleClass = 'scale-150 ';
37+
38+ $: focused = index === focusedSymbolIndex;
39+ $: focusedClasses = (
40+ focused ? `${scaleClass} z-10`
41+ : (focusedSymbolIndex !== undefined ? 'scale-50' : '' )
42+ );
43+ // $: focusedClasses = focused ? `border border-gold-s1 border-2` : '';
1444
1545 function onPartDelete({ detail: partId }) {
1646 symbolQuery = symbolQuery.removePart(partId);
@@ -21,32 +51,48 @@
2151 }
2252
2353 let filteredInputComponents = [];
54+ let inputContainsStrokes;
2455 $: {
2556 if (inputComponents) {
2657 filteredInputComponents = without(inputComponents, ...symbolQuery.components);
2758 } else {
2859 filteredInputComponents = [];
2960 }
61+ filteredInputComponents = filterPartsByGeon(filteredInputComponents, symbolQuery.geon);
3062 }
63+ $: {
64+ inputContainsStrokes = undefined !== filteredInputComponents.find((c) => {
65+ return c[0] === 'l' || c[0] === 'a' || c[0] === 'b';
66+ });
67+ }
68+ $: plausible = $searchSettings.allowFictionalSigils || symbolQuery.isPlausible;
3169</script>
3270
33−<div class="aspect-square relative flex {focusedClasses}" style:max-width="{size}px" on:click>
71+<div class="aspect-square relative flex transition {translateClass} {focusedClasses}"
72+ style:max-width="{size}px"
73+ on:click|stopPropagation
74+>
3475 {#if symbolQuery.components.length > 0}
3576 <Symbol components={symbolQuery.components} {size}
3677 inverted={!symbolQuery.geon}
3778 interactive={focused}
79+ fgColor={plausible ? 'black' : 'orange'}
3880 hoverColor="red"
3981 on:partClick={onPartDelete}
4082 />
4183 {:else}
42− <div class="m-1 rounded-sm border-4 border-gray-500 grow"></div>
84+ <div class="border-[1.5px] border-white grow bg-white">
85+ <div class="rounded-md border-4 border-gray-500 h-full"></div>
86+ </div>
4387 {/if}
4488 {#if focused && filteredInputComponents.length}
4589 <div class="absolute inset-0 pointer-events-none">
4690 <Symbol components={filteredInputComponents} {size}
47− strokeWidthFactor={2.5}
48− fgColor={symbolQuery.geon ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)"}
49− bgColor="transparent"
91+ strokeWidthFactor={inputContainsStrokes ? 2.5 : 1}
92+ fgColor={symbolQuery.geon ? "white" : "black"}
93+ bgColor={!symbolQuery.geon ? "white" : "black"}
94+ altBgColor="transparent"
95+ opacity={0.5}
5096 inverted
5197 interactive
5298 on:partClick={onPartClick}
ui/src/components/search/sigil/SymbolInputModeSelector.svelte+40−144
@@ -2,133 +2,24 @@
22 import GeonSelector from './GeonSelector.svelte';
33 import SymbolInputModeSelectorButton from './SymbolInputModeSelectorButton.svelte';
44
5− import Symbol from './Symbol.svelte';
5+ import { searchSettings } from 'stores/searchStores';
6+ import type { PartType, ModeDef, ModeDefs } from 'types/sigil';
7+ import type { SymbolQuery } from 'lib/sigil';
8+ import { MODE_DEFS } from 'types/sigil';
9+ import { filterPartsByGeon, getDeepParts } from 'lib/sigil';
610
7− import type { PartType, SymbolQuery } from 'types/sigil';
11+ import upArrow from 'assets/up-arrow.svg';
12+ import xIcon from 'assets/x.svg';
813
914 export let symbolQuery: SymbolQuery;
10− // export let inputMode: PartType;
1115 export let inputComponents: string[] = [];
1216 export let unfocusSymbol: Function;
1317
14− interface ModeDefs {
15− [mode: string]: ModeDef
16− }
17− interface ModeDef {
18− modes?: ModeDefs;
19− displayParts?: string[];
20− parts?: string[];
21− }
22−
23− const modeDefs: ModeDefs = {
24− geon: {
25− modes: {},
26− displayParts: ['ghb'],
27− },
28− line: {
29− modes: {
30− mainFulls: {
31− parts: ['lvf', 'lhf', 'lff', 'lbf'],
32− },
33− halvesFulls: {
34− parts: ['lvfl', 'lvfr', 'lhft', 'lhfb', 'lfht', 'lfhb', 'lbht', 'lbhb'],
35− },
36− forwards: {
37− parts: ['lff', 'lfft', 'lffb'],
38− },
39− backwards: {
40− parts: ['lbf', 'lbft'],
41− },
42− weirds: {
43− parts: ['lbhtf', 'lbrf', 'lfhm'],
44− }
45− },
46− parts: ['lvf', 'lvfl', 'lvfr', 'lhf', 'lhft', 'lhfb', 'lff', 'lbf'],
47− },
48− arcCorner: {
49− modes: {
50− trFulls: {
51− parts: ['atrf8', 'atrf6', 'atrf4', 'atrf2'],
52− },
53− trHalves: {
54− parts: [/*'atra8',*/ 'atra6', 'atra4', 'atra2', 'atrb8', 'atrb6', 'atrb4', 'atrb2'],
55− displayParts: ['atrb8', 'atrb6', 'atrb4', 'atrb2'],
56− },
57− tlFulls: {
58− parts: ['atlf8', 'atlf6', 'atlf4', 'atlf2'],
59− },
60− tlHalves: {
61− parts: ['atla8', 'atla6', 'atla4', 'atla2', 'atlb8', 'atlb6', 'atlb4', 'atlb2'],
62− displayParts: ['atlb8', 'atlb6', 'atlb4', 'atlb2'],
63− },
64− blFulls: {
65− parts: ['ablf8', 'ablf6', 'ablf4', 'ablf2'],
66− },
67− blHalves: {
68− parts: ['ablb8', 'ablb6', 'ablb4', 'ablb2'],
69− displayParts: ['ablb8', 'ablb6', 'ablb4', 'ablb2'],
70− },
71− brFulls: {
72− parts: ['abrf8', 'abrf6', 'abrf4', 'abrf2'],
73− },
74− brHalves: {
75− parts: ['abrb8', 'abrb6', 'abrb4', 'abrb2'],
76− displayParts: ['abrb8', 'abrb6', 'abrb4', 'abrb2'],
77− },
78− },
79− parts: ['ablf8', 'ablf6', 'ablf4', 'ablf2'],
80− },
81− arc: {
82− modes: {
83− rings: {
84− parts: ['am081', 'am082', 'am083', 'am08s'],
85− },
86− diagonals: {
87− parts: ['am741', 'am742', 'am743', 'am422', 'am423'],
88− },
89− topsBottoms: {
90− parts: ['ab2', 'ab3', 'ab4', 'at1', 'at3', 'at4'],
91− },
92− leftsRights: {
93− parts: ['al2', 'al3', 'al4', 'ar1', 'ar2', 'ar4'/*, 'arb2'*/],
94− },
95− rights: {
96− parts: ['am642', 'am643'],
97− },
98− middles: {
99− parts: ['am222', 'am223', 'am622', 'am623'],
100− },
101− middless: {
102− parts: ['am142', 'am443'],
103− },
104− topRights: {
105− parts: ['am021', 'am022', 'am023', 'am242', 'arb2'],
106− },
107− // weirds: {
108− // parts: ['arb2'],
109− // },
110− },
111− parts: ['am222', 'am223', 'ar4', 'ar2', 'ar1'],
112− },
113− circle: {
114− parts: [
115− 'cbr3mb', 'ctl3mb',
116− 'cmmb',
117− 'ct2mb', 'cl2mb', 'cb2mb', 'cr2mb',
118− ],
119− },
120− donut: {
121− parts: ['dbl', /*'dl',*/ 'dm', 'dtl', 'dtr'],
122− },
123− bezier: {
124− parts: ['b1l1', 'b1l2', 'b1l3'],
125− },
126− };
12718 let modes: string[] = [];
12819 let modeDef: ModeDef = undefined;
129− let subModeDefs: ModeDefs = modeDefs;
130− let size = 48;
20+ let subModeDefs: ModeDefs = MODE_DEFS;
13121
22+ let buttonClasses = 'border border-gray-500 rounded-md aspect-square w-[48px]';
13223
13324 $: {
13425 if (!symbolQuery.geon) {
@@ -158,7 +49,7 @@
15849
15950 $: {
16051 modeDef = undefined;
161− subModeDefs = modeDefs;
52+ subModeDefs = MODE_DEFS;
16253 for (let mode of modes) {
16354 if (modeDef && modeDef.modes) {
16455 subModeDefs = modeDef.modes;
@@ -176,6 +67,9 @@
17667
17768 if (modeDef?.parts?.length && !modeDef.modes) {
17869 inputComponents = modeDef.parts;
70+ if (!$searchSettings.allowFictionalSigils) {
71+ inputComponents = symbolQuery.filterPlausibleParts(inputComponents);
72+ }
17973 } else {
18074 inputComponents = [];
18175 }
@@ -190,32 +84,34 @@
19084 }
19185</script>
19286
193−<div class="grid grid-rows-2 grid-flow-col xs:grid-cols-2 xs:grid-flow-row place-content-center justify-center">
194− <div
195− class="aspect-square"
196− on:click={popMode}
197− >
198− done
87+<style lang="scss">
88+ .grid-cols-custom {
89+ grid-template-columns: repeat(auto-fill, 48px);
90+ }
91+</style>
92+
93+<div class="mx-auto flex items-center justify-center xs:flex-col">
94+ <div class="grid grid-cols-custom gap-1 self-start justify-center mr-1 xs:mr-0 xs:mb-1 xs:self-stretch">
95+ <img src={upArrow} alt="done" class={buttonClasses} on:click={popMode}/>
96+ <img src={xIcon} alt="done" class={buttonClasses} on:click={clear}/>
19997 </div>
200− <div
201− class="aspect-square"
202− on:click={clear}
203− >
204− clear
98+ <div class="max-w-[15rem] grid grid-cols-custom gap-1 justify-center xs:max-w-[8rem]">
99+ {#if modes[0] === 'geon'}
100+ <GeonSelector bind:symbolQuery {popMode} />
101+ {:else}
102+ {#each Object.entries(subModeDefs) as [modeName, def]}
103+ {@const parts = filterPartsByGeon(def.displayParts || def.parts, symbolQuery.geon)}
104+ {@const deepParts = getDeepParts(def)}
105+ {@const plausible = $searchSettings.allowFictionalSigils || symbolQuery.arePartsPlausible(deepParts)}
106+ {#if plausible || modeName === 'geon'}
107+ <SymbolInputModeSelectorButton components={parts} focused={modes.slice(-1)[0] === modeName}
108+ on:click={() => pushMode(modeName)}
109+ />
110+ <!-- {:else}
111+ {deepParts.join()} -->
112+ {/if}
113+ {/each}
114+ {/if}
205115 </div>
206− {#if modes[0] === 'geon'}
207− <GeonSelector bind:symbolQuery {popMode} />
208− {:else}
209− <!-- {#each PART_TYPES as partType}
210− <SymbolInputModeSelectorButton {partType} focused={modes[0] === partType}
211− on:click={() => pushMode(partType)}
212− />
213− {/each} -->
214− {#each Object.entries(subModeDefs) as [modeName, def]}
215− <SymbolInputModeSelectorButton components={def.displayParts || def.parts} focused={modes.slice(-1)[0] === modeName}
216− on:click={() => pushMode(modeName)}
217− />
218− {/each}
219− {/if}
220116</div>
221117
ui/src/components/search/sigil/SymbolInputModeSelectorButton.svelte+24−32
@@ -1,41 +1,33 @@
11<script lang="ts">
2+ import { createEventDispatcher } from 'svelte';
23 import Symbol from './Symbol.svelte';
34
4− import type { PartType, SymbolQuery } from 'types/sigil';
5− import { PART_TYPES } from 'types/sigil';
6−
7− // export let partType: PartType;
85 export let components: string[] = [];
96 export let focused: boolean = false;
10−
7+ export let enabled: boolean = true;
8+ export let padding: number = undefined;
9+
10+ const dispatch = createEventDispatcher();
1111 let size = 48;
12− $: focusedClasses = focused ? 'border border-gold-s1 border-2' : '';
13− $: includesGeon = components.find(c => c[0] === 'g') === undefined;
14−</script>
1512
16−<div class="border-2 w-[48px] {focusedClasses}" on:click>
17− <Symbol {components} {size} inverted={includesGeon} />
18− <!-- {#if partType === 'geon'}
19− <Symbol components={['ghb']} {size} />
20− {:else if partType === 'line'}
21− <Symbol components={['lvf', 'lvfl', 'lvfr', 'lhf', 'lhft', 'lhfb', 'lff', 'lbf']} {size} inverted />
22− {:else if partType === 'arc'}
23− <Symbol components={['abl202', 'abl402', 'abl602', 'abl802', 'ar124', 'ar224', 'ar424' ]} {size} inverted />
24− {:else if partType === 'ring'}
25− <Symbol components={['rm1', 'rm2', 'rm3']} {size} inverted />
26− {:else if partType === 'circle'}
27− <Symbol components={[
28− // 'ctr1mb', 'cbr1mb', 'cbl1mb', 'ctl1mb',
29− 'cbr3mb', 'ctl3mb',
30− 'cmmb',
31− 'ct2mb', 'cl2mb', 'cb2mb', 'cr2mb',
32− ]} {size} inverted />
33− {:else if partType === 'donut'}
34− <Symbol components={['dbl', /*'dl',*/ 'dm', 'dtl', 'dtr']} {size} inverted />
35− {:else if partType === 'bezier'}
36− <Symbol components={['b1l1', 'b1l2', 'b1l3']} {size} inverted />
37− {:else}
38− not {partType}
39− {/if} -->
13+ $: focusedClasses = focused ? 'border-2 border-gold-s1 p-[3px]' : 'border border-gray-500 p-[4px]';
14+ $: enabledClasses = enabled ? '' : 'opacity-50';
15+ $: includesGeon = components.find(c => c[0] === 'g') !== undefined;
16+ $: defaultPadding = includesGeon ? 0 : 0;
17+ $: actualPadding = padding !== undefined ? padding : defaultPadding;
18+
19+ function onClick(e) {
20+ if (enabled) dispatch('click', e);
21+ }
22+</script>
23+<div
24+class="bg-white rounded-md w-[48px] {focusedClasses} {enabledClasses}"
25+on:click={onClick}
26+>
27+<div class:cursor-pointer={enabled}
28+ style:padding="{actualPadding}px"
29+>
30+ <Symbol {components} {size} inverted={!includesGeon} />
31+</div>
4032</div>
4133
ui/src/lib/sigil.ts+197−8
@@ -1,9 +1,20 @@
1−import type { Clan } from 'types/sigil';
2−import { SymbolQuery } from 'types/sigil';
1+import intersection from 'lodash/intersection';
2+import without from 'lodash/without';
3+import uniq from 'lodash/uniq';
4+import { partParents, symbolDefs } from '@johnhyde/sigil-js';
5+
6+import type { Clan, Geon, ModeDef, PartType, SymbolType } from 'types/sigil';
7+import { PART_TYPES } from 'types/sigil';
8+import { prefixes, suffixes } from 'lib/id';
9+
10+export function sigilScalingFunction(size: number): number {
11+ return 3.5 * Math.sqrt(51 / size);
12+}
313
414export class SigilQuery {
515 clan: Clan;
616 symbols: SymbolQuery[];
17+ allowFictional: boolean = false;
718
819 constructor(clan: Clan = 'planet', symbols = []) {
920 this.clan = clan;
@@ -13,19 +24,18 @@ export class SigilQuery {
1324
1425 padSymbols() {
1526 if (this.symbols.length === 1) {
16− this.symbols.unshift(new SymbolQuery());
27+ this.symbols.unshift(new SymbolQuery(this));
1728 }
1829 while (this.symbols.length < 4) {
19− this.symbols.push(new SymbolQuery());
30+ this.symbols.push(new SymbolQuery(this));
2031 }
2132 this.symbols = this.symbols.slice(0, 4);
2233 this.symbols.forEach((symbol, i) => {
2334 if (i % 2 === 0) {
2435 symbol.symbolType = 'prefix';
25− // symbol.components = ['gci', 'abr622']
2636 } else {
2737 symbol.symbolType = 'suffix';
28− symbol.components = ['gsq', 'ablf2']
38+ // symbol.components = ['gsq', 'ablf2']
2939 }
3040 });
3141 this.symbols = this.symbols;
@@ -39,6 +49,10 @@ export class SigilQuery {
3949 return this.symbols;
4050 }
4151
52+ get isPlausible() {
53+ return this.activeSymbols.every(symbol => !symbol.isPlausible);
54+ }
55+
4256 get string(): string {
4357 return this.symbols.map((symbol) => symbol.components.join()).join(' | ')
4458 }
@@ -50,6 +64,181 @@ export class SigilQuery {
5064 }
5165}
5266
53−export function sigilScalingFunction(size: number): number {
54− return 3.5 * Math.sqrt(51 / size);
67+export function getPlausibleSyllables(parts: string[], symbolType: SymbolType): string[] {
68+ let plausibleSyllables: string[];
69+ if (symbolType === 'prefix') {
70+ plausibleSyllables = prefixes;
71+ } else if (symbolType === 'suffix') {
72+ plausibleSyllables = suffixes;
73+ } else {
74+ plausibleSyllables = [...prefixes, ...suffixes];
75+ }
76+ const possibilitiesByPart = parts.map((partId) => partParents[partId]);
77+ plausibleSyllables = intersection(plausibleSyllables, ...possibilitiesByPart);
78+ return plausibleSyllables;
79+}
80+
81+export function getPlausibleNewParts(parts: string[], symbolType: SymbolType): string[] {
82+ const syllables: string[] = getPlausibleSyllables(parts, symbolType);
83+ let plausibleParts = uniq(syllables.map((syllable) => symbolDefs[syllable]).flat());
84+ return without(plausibleParts, ...parts);
85+}
86+
87+export function filterPlausibleParts(parts: string[], symbolType: SymbolType, newParts: string[]): string[] {
88+ const plausibleParts = getPlausibleNewParts(parts, symbolType);
89+ return intersection(newParts, plausibleParts);
90+}
91+
92+export function arePartsPlausible(parts: string[], symbolType: SymbolType, newParts: string[]): boolean {
93+ return filterPlausibleParts(parts, symbolType, newParts).length > 0;
94+}
95+
96+function hasComplement(partId: string): boolean {
97+ const secondToLastChar = partId.slice(-2)[0];
98+ return partId[0] === 'c' && (secondToLastChar === 'm' || secondToLastChar === 'l');
99+}
100+
101+function getComplementaryCircleId(circleId: string): string {
102+ let newId = circleId.replace(/m(?=.$)/, 'l');
103+ if (newId === circleId) {
104+ newId = circleId.replace(/l(?=.$)/, 'm');
105+ }
106+ circleId = newId;
107+ circleId = circleId.replace(/f$/, 'b');
108+ if (newId === circleId) {
109+ circleId = circleId.replace(/b$/, 'f');
110+ }
111+ return circleId;
112+}
113+
114+function getPartTypeIndex(partId: string): number {
115+ return PART_TYPES.findIndex(p => p[0] === partId[0]);
116+}
117+
118+export function filterPartsByGeon(parts: string[], geon: Geon): string[] {
119+ if (['gq1', 'gbs'].includes(geon)) {
120+ parts = without(parts, 'ctr2mb', 'ctr2lf', 'atrf2', 'atra2', 'atrb2');
121+ } else {
122+ parts = without(parts, 'ctr3mf', 'ctr3lb');
123+ }
124+ if (['gq2', 'gfs'].includes(geon)) {
125+ parts = without(parts, 'ctl3mb', 'ctl3lf', 'ctl2mb', 'ctl2lf', 'atlf2', 'atla2', 'atlb2');
126+ } else {
127+ parts = without(parts, 'ctl3mf', 'ctl3lb');
128+ }
129+ if (['gq3', 'gbs'].includes(geon)) {
130+ parts = without(parts, 'cbl3mb', 'cbl3lf', 'cbl2mb', 'cbl2lf', 'ablf2', 'ablb2');
131+ } else {
132+ parts = without(parts, 'cbl3mf', 'cbl3lb');
133+ }
134+ if (['gq4', 'gfs'].includes(geon)) {
135+ parts = without(parts, 'cbr2mb', 'cbr2lf', 'abrf2', 'abrb2', 'arb2');
136+ }
137+ return parts;
138+}
139+
140+export class SymbolQuery {
141+ sigilQuery?: SigilQuery;
142+ symbolType: SymbolType;
143+ components: string[];
144+
145+ constructor(sigilQuery: SigilQuery = undefined, components = [], symbolType: SymbolType = 'suffix') {
146+ this.sigilQuery = sigilQuery;
147+ this.components = components;
148+ this.symbolType = symbolType;
149+ this.cleanupParts();
150+ }
151+
152+ set geon(geon: Geon) {
153+ this.components = this.components.filter(c => c[0] !== 'g');
154+ if (geon)
155+ this.components = [geon, ...this.components];
156+ this.cleanupParts();
157+ }
158+
159+ get geon(): Geon {
160+ return this.components.find(c => c[0] === 'g');
161+ }
162+
163+ get allowFictional() {
164+ if (this.sigilQuery) return this.sigilQuery.allowFictional;
165+ return true;
166+ }
167+
168+ get plausibleSyllables() {
169+ return getPlausibleSyllables(this.components, this.symbolType);
170+ }
171+
172+ get isPlausible() {
173+ return this.plausibleSyllables.length > 0;
174+ }
175+
176+ get plausibleParts() {
177+ return getPlausibleNewParts(this.components, this.symbolType);
178+ }
179+
180+ filterPlausibleParts(parts: string[]): string[] {
181+ return filterPlausibleParts(this.components, this.symbolType, parts);
182+ }
183+
184+ arePartsPlausible(parts: string[]): boolean {
185+ return arePartsPlausible(this.components, this.symbolType, parts);
186+ }
187+
188+ cleanupParts() {
189+ this.components = uniq(this.components);
190+ this.components = filterPartsByGeon(this.components, this.geon);
191+ // if (!this.allowFictional) {
192+ // this.components = this.plausibleParts;
193+ // }
194+ this.components = this.components.sort((c1, c2) => {
195+ let i1 = getPartTypeIndex(c1);
196+ let i2 = getPartTypeIndex(c2);
197+ let diff = i1 - i2;
198+ if (diff !== 0) return diff;
199+ if (c1[0] === 'c') {
200+ return c1.localeCompare(c2);
201+ } else {
202+ return c2.localeCompare(c1);
203+ }
204+ });
205+ }
206+
207+ clearPartType(partType: PartType) {
208+ this.components = this.components.filter(c => c[0] !== partType[0]);
209+ return this;
210+ }
211+
212+ clear() {
213+ this.components = [];
214+ return this;
215+ }
216+
217+ addPart(partId: string) {
218+ this.components = [...this.components, partId];
219+ if (hasComplement(partId)) {
220+ const otherPartId = getComplementaryCircleId(partId);
221+ this.components = [...this.components, otherPartId];
222+ }
223+ this.cleanupParts();
224+ return this;
225+ }
226+
227+ removePart(partId: string) {
228+ this.components = without(this.components, partId);
229+ if (hasComplement(partId)) {
230+ const otherPartId = getComplementaryCircleId(partId);
231+ this.components = without(this.components, otherPartId);
232+ }
233+ this.cleanupParts();
234+ return this;
235+ }
236+}
237+
238+export function getDeepParts(def: ModeDef): string[] {
239+ if (def.modes) {
240+ return uniq(Object.values(def.modes).map(getDeepParts).flat());
241+ } else {
242+ return def.parts || [];
243+ }
55244}
ui/src/styles/tailwind-output.css+207
@@ -559,6 +559,12 @@ Ensure the default browser behavior of the `hidden` attribute.
559559.z-50 {
560560 z-index: 50;
561561}
562+.col-span-3 {
563+ grid-column: span 3 / span 3;
564+}
565+.col-span-2 {
566+ grid-column: span 2 / span 2;
567+}
562568.m-4 {
563569 margin: 1rem;
564570}
@@ -687,6 +693,12 @@ Ensure the default browser behavior of the `hidden` attribute.
687693.-mt-0\.5 {
688694 margin-top: -0.125rem;
689695}
696+.mb-1 {
697+ margin-bottom: 0.25rem;
698+}
699+.mr-1 {
700+ margin-right: 0.25rem;
701+}
690702.block {
691703 display: block;
692704}
@@ -780,6 +792,9 @@ Ensure the default browser behavior of the `hidden` attribute.
780792.min-h-0 {
781793 min-height: 0px;
782794}
795+.min-h-\[14rem\] {
796+ min-height: 14rem;
797+}
783798.w-full {
784799 width: 100%;
785800}
@@ -840,6 +855,9 @@ Ensure the default browser behavior of the `hidden` attribute.
840855.w-\[48px\] {
841856 width: 48px;
842857}
858+.w-\[\] {
859+ width: ;
860+}
843861.min-w-0 {
844862 min-width: 0px;
845863}
@@ -864,6 +882,21 @@ Ensure the default browser behavior of the `hidden` attribute.
864882.max-w-\[18rem\] {
865883 max-width: 18rem;
866884}
885+.max-w-\[10rem\] {
886+ max-width: 10rem;
887+}
888+.max-w-\[21rem\] {
889+ max-width: 21rem;
890+}
891+.max-w-\[20rem\] {
892+ max-width: 20rem;
893+}
894+.max-w-\[19rem\] {
895+ max-width: 19rem;
896+}
897+.max-w-\[15rem\] {
898+ max-width: 15rem;
899+}
867900.flex-shrink-0 {
868901 flex-shrink: 0;
869902}
@@ -885,6 +918,48 @@ Ensure the default browser behavior of the `hidden` attribute.
885918.border-collapse {
886919 border-collapse: collapse;
887920}
921+.origin-bottom-right {
922+ transform-origin: bottom right;
923+}
924+.origin-left {
925+ transform-origin: left;
926+}
927+.origin-right {
928+ transform-origin: right;
929+}
930+.origin-top-left {
931+ transform-origin: top left;
932+}
933+.origin-top-right {
934+ transform-origin: top right;
935+}
936+.origin-bottom-left {
937+ transform-origin: bottom left;
938+}
939+.translate-x-1\/4 {
940+ --tw-translate-x: 25%;
941+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
942+}
943+.translate-y-1\/4 {
944+ --tw-translate-y: 25%;
945+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
946+}
947+.translate-x-0 {
948+ --tw-translate-x: 0px;
949+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
950+}
951+.translate-y-0 {
952+ --tw-translate-y: 0px;
953+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
954+}
955+.-translate-x-1\/4 {
956+ --tw-translate-x: -25%;
957+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
958+}
959+.-translate-y-1\/4 {
960+ --tw-translate-y: -25%;
961+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
962+}
888963.rotate-45 {
889964 --tw-rotate: 45deg;
890965 transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -893,6 +968,16 @@ Ensure the default browser behavior of the `hidden` attribute.
893968 --tw-rotate: -45deg;
894969 transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
895970}
971+.scale-150 {
972+ --tw-scale-x: 1.5;
973+ --tw-scale-y: 1.5;
974+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
975+}
976+.scale-50 {
977+ --tw-scale-x: .5;
978+ --tw-scale-y: .5;
979+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
980+}
896981.transform {
897982 transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
898983}
@@ -918,9 +1003,26 @@ Ensure the default browser behavior of the `hidden` attribute.
9181003.resize {
9191004 resize: both;
9201005}
1006+.auto-cols-min {
1007+ grid-auto-columns: -webkit-min-content;
1008+ grid-auto-columns: min-content;
1009+}
1010+.auto-cols-fr {
1011+ grid-auto-columns: minmax(0, 1fr);
1012+}
1013+.grid-flow-row {
1014+ grid-auto-flow: row;
1015+}
9211016.grid-flow-col {
9221017 grid-auto-flow: column;
9231018}
1019+.grid-flow-col-dense {
1020+ grid-auto-flow: column dense;
1021+}
1022+.auto-rows-min {
1023+ grid-auto-rows: -webkit-min-content;
1024+ grid-auto-rows: min-content;
1025+}
9241026.grid-cols-3 {
9251027 grid-template-columns: repeat(3, minmax(0, 1fr));
9261028}
@@ -930,6 +1032,12 @@ Ensure the default browser behavior of the `hidden` attribute.
9301032.grid-cols-1 {
9311033 grid-template-columns: repeat(1, minmax(0, 1fr));
9321034}
1035+.grid-cols-4 {
1036+ grid-template-columns: repeat(4, minmax(0, 1fr));
1037+}
1038+.grid-cols-6 {
1039+ grid-template-columns: repeat(6, minmax(0, 1fr));
1040+}
9331041.grid-rows-2 {
9341042 grid-template-rows: repeat(2, minmax(0, 1fr));
9351043}
@@ -951,6 +1059,9 @@ Ensure the default browser behavior of the `hidden` attribute.
9511059.content-center {
9521060 align-content: center;
9531061}
1062+.items-start {
1063+ align-items: flex-start;
1064+}
9541065.items-center {
9551066 align-items: center;
9561067}
@@ -1033,6 +1144,12 @@ Ensure the default browser behavior of the `hidden` attribute.
10331144 --tw-divide-opacity: 1;
10341145 border-color: rgb(252 211 77 / var(--tw-divide-opacity));
10351146}
1147+.place-self-center {
1148+ place-self: center;
1149+}
1150+.self-start {
1151+ align-self: flex-start;
1152+}
10361153.self-center {
10371154 align-self: center;
10381155}
@@ -1110,6 +1227,12 @@ Ensure the default browser behavior of the `hidden` attribute.
11101227.border-2 {
11111228 border-width: 2px;
11121229}
1230+.border-\[1\.5px\] {
1231+ border-width: 1.5px;
1232+}
1233+.border-0 {
1234+ border-width: 0px;
1235+}
11131236.border-l-8 {
11141237 border-left-width: 8px;
11151238}
@@ -1293,6 +1416,10 @@ Ensure the default browser behavior of the `hidden` attribute.
12931416 --tw-bg-opacity: 1;
12941417 background-color: rgb(191 219 254 / var(--tw-bg-opacity));
12951418}
1419+.bg-gray-500 {
1420+ --tw-bg-opacity: 1;
1421+ background-color: rgb(107 114 128 / var(--tw-bg-opacity));
1422+}
12961423.fill-transparent {
12971424 fill: transparent;
12981425}
@@ -1317,6 +1444,27 @@ Ensure the default browser behavior of the `hidden` attribute.
13171444.p-1 {
13181445 padding: 0.25rem;
13191446}
1447+.p-0 {
1448+ padding: 0px;
1449+}
1450+.p-0\.5 {
1451+ padding: 0.125rem;
1452+}
1453+.p-\[3px\] {
1454+ padding: 3px;
1455+}
1456+.p-\[4px\] {
1457+ padding: 4px;
1458+}
1459+.p-\[2\.5px\] {
1460+ padding: 2.5px;
1461+}
1462+.p-\[2\.75px\] {
1463+ padding: 2.75px;
1464+}
1465+.p-\[2px\] {
1466+ padding: 2px;
1467+}
13201468.px-2 {
13211469 padding-left: 0.5rem;
13221470 padding-right: 0.5rem;
@@ -1539,6 +1687,14 @@ Ensure the default browser behavior of the `hidden` attribute.
15391687 --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
15401688 box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
15411689}
1690+.drop-shadow {
1691+ --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));
1692+ 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);
1693+}
1694+.sepia {
1695+ --tw-sepia: sepia(100%);
1696+ 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);
1697+}
15421698.filter {
15431699 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);
15441700}
@@ -1557,6 +1713,13 @@ Ensure the default browser behavior of the `hidden` attribute.
15571713 transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
15581714 transition-duration: 150ms;
15591715}
1716+.transition {
1717+ transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
1718+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1719+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
1720+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1721+ transition-duration: 150ms;
1722+}
15601723.hover\:bg-gray-200:hover {
15611724 --tw-bg-opacity: 1;
15621725 background-color: rgb(229 231 235 / var(--tw-bg-opacity));
@@ -1604,22 +1767,58 @@ Ensure the default browser behavior of the `hidden` attribute.
16041767 margin: 0px;
16051768 }
16061769
1770+ .xs\:mr-0 {
1771+ margin-right: 0px;
1772+ }
1773+
1774+ .xs\:mb-1 {
1775+ margin-bottom: 0.25rem;
1776+ }
1777+
16071778 .xs\:max-w-\[18rem\] {
16081779 max-width: 18rem;
16091780 }
16101781
1782+ .xs\:max-w-\[8rem\] {
1783+ max-width: 8rem;
1784+ }
1785+
16111786 .xs\:grid-flow-row {
16121787 grid-auto-flow: row;
16131788 }
16141789
1790+ .xs\:grid-flow-col {
1791+ grid-auto-flow: column;
1792+ }
1793+
1794+ .xs\:grid-flow-row-dense {
1795+ grid-auto-flow: row dense;
1796+ }
1797+
16151798 .xs\:grid-cols-2 {
16161799 grid-template-columns: repeat(2, minmax(0, 1fr));
16171800 }
16181801
1802+ .xs\:grid-rows-3 {
1803+ grid-template-rows: repeat(3, minmax(0, 1fr));
1804+ }
1805+
16191806 .xs\:flex-row {
16201807 flex-direction: row;
16211808 }
16221809
1810+ .xs\:flex-col {
1811+ flex-direction: column;
1812+ }
1813+
1814+ .xs\:items-stretch {
1815+ align-items: stretch;
1816+ }
1817+
1818+ .xs\:self-stretch {
1819+ align-self: stretch;
1820+ }
1821+
16231822 .xs\:rounded-bl-2xl {
16241823 border-bottom-left-radius: 1rem;
16251824 }
@@ -1627,6 +1826,14 @@ Ensure the default browser behavior of the `hidden` attribute.
16271826 .xs\:rounded-bl-none {
16281827 border-bottom-left-radius: 0px;
16291828 }
1829+
1830+ .xs\:border-l {
1831+ border-left-width: 1px;
1832+ }
1833+
1834+ .xs\:border-t-0 {
1835+ border-top-width: 0px;
1836+ }
16301837}
16311838@media (min-width: 768px) {
16321839
ui/src/types/sigil.ts+132−80
@@ -1,10 +1,7 @@
1−import without from 'lodash/without';
2−import uniq from 'lodash/uniq';
3−
41export const CLANS = ['galaxy', 'star', 'planet'] as const;
52export type Clan = typeof CLANS[number];
63
7−export const PART_TYPES = ['geon', 'line', 'arc', 'circle', 'donut', 'bezier'] as const;
4+export const PART_TYPES = ['geon', 'line', 'arc', 'bezier', 'circle', 'donut'] as const;
85export type PartType = typeof PART_TYPES[number];
96
107export const GEONS = ['gci', 'gsq', 'ghl', 'ghr', 'ght', 'ghb', 'gq1', 'gq2', 'gq3', 'gq4', 'gfs', 'gbs'];
@@ -12,85 +9,140 @@ export type Geon = typeof GEONS[number];
129
1310export type SymbolType = ('prefix' | 'suffix');
1411
15−function getComplementaryCircleId(circleId: string): string {
16− let newId = circleId.replace(/m(?=.$)/, 'l');
17− if (newId === circleId) {
18− newId = circleId.replace(/l(?=.$)/, 'm');
19− }
20− circleId = newId;
21− circleId = circleId.replace(/f$/, 'b');
22− if (newId === circleId) {
23− circleId = circleId.replace(/b$/, 'f');
24− }
25− return circleId;
12+export interface ModeDef {
13+ modes?: ModeDefs;
14+ displayParts?: string[];
15+ parts?: string[];
2616}
2717
28−function getPartTypeIndex(partId: string): number {
29− return PART_TYPES.findIndex(p => p[0] === partId[0]);
18+export interface ModeDefs {
19+ [mode: string]: ModeDef
3020}
3121
32−export class SymbolQuery {
33− symbolType: SymbolType;
34− components: string[];
35−
36− constructor(symbolType: SymbolType = 'suffix', components = []) {
37− this.symbolType = symbolType;
38− this.components = components;
39− this.cleanupParts();
40− }
41−
42− set geon(geon: Geon) {
43− this.components = this.components.filter(c => c[0] !== 'g');
44− if (geon)
45− this.components = [geon, ...this.components];
46− }
4722
48− get geon(): Geon {
49− return this.components.find(c => c[0] === 'g');
50− }
51−
52− cleanupParts() {
53− this.components = uniq(this.components);
54− this.components = this.components.sort((c1, c2) => {
55− let i1 = getPartTypeIndex(c1);
56− let i2 = getPartTypeIndex(c2);
57− let diff = i1 - i2;
58− if (diff !== 0) return diff;
59− if (c1[0] === 'c') {
60− return c1.localeCompare(c2);
61− } else {
62− return c2.localeCompare(c1);
23+export const MODE_DEFS: ModeDefs = {
24+ geon: {
25+ modes: {},
26+ displayParts: ['ghb'],
27+ },
28+ line: {
29+ modes: {
30+ mainFulls: {
31+ parts: ['lvf', 'lhf', 'lff', 'lbf'],
32+ },
33+ halvesFulls: {
34+ parts: ['lvfl', 'lvfr', 'lhft', 'lhfb', 'lfht', 'lfhb', 'lbht', 'lbhb'],
35+ },
36+ forwards: {
37+ parts: ['lff', 'lfft', 'lffb'],
38+ },
39+ backwards: {
40+ parts: ['lbf', 'lbft'],
41+ },
42+ weirds: {
43+ parts: ['lbhtf', 'lbrf', 'lfhm'],
6344 }
64− });
65− }
66−
67− clearPartType(partType: PartType) {
68− this.components = this.components.filter(c => c[0] !== partType[0]);
69− return this;
70− }
71−
72− clear() {
73− this.components = [];
74− return this;
75− }
76−
77− addPart(partId: string) {
78− this.components = [...this.components, partId];
79− if (partId[0] === 'c') {
80− const otherPartId = getComplementaryCircleId(partId);
81− this.components = [...this.components, otherPartId];
82− }
83− this.cleanupParts();
84− return this;
85− }
86−
87− removePart(partId: string) {
88− this.components = without(this.components, partId);
89− if (partId[0] === 'c') {
90− const otherPartId = getComplementaryCircleId(partId);
91− this.components = without(this.components, otherPartId);
92− }
93− this.cleanupParts();
94− return this;
95− }
96−}
45+ },
46+ parts: ['lvf', 'lvfl', 'lvfr', 'lhf', 'lhft', 'lhfb', 'lff', 'lbf'],
47+ },
48+ arcCorner: {
49+ modes: {
50+ trFulls: {
51+ parts: ['atrf8', 'atrf6', 'atrf4', 'atrf2'],
52+ },
53+ trHalves: {
54+ parts: [/*'atra8',*/ 'atra6', 'atra4', 'atra2', 'atrb8', 'atrb6', 'atrb4', 'atrb2'],
55+ displayParts: ['atrb8', 'atrb6', 'atrb4', 'atrb2'],
56+ },
57+ tlFulls: {
58+ parts: ['atlf8', 'atlf6', 'atlf4', 'atlf2'],
59+ },
60+ tlHalves: {
61+ parts: ['atla8', 'atla6', 'atla4', 'atla2', 'atlb8', 'atlb6', 'atlb4', 'atlb2'],
62+ displayParts: ['atlb8', 'atlb6', 'atlb4', 'atlb2'],
63+ },
64+ blFulls: {
65+ parts: ['ablf8', 'ablf6', 'ablf4', 'ablf2'],
66+ },
67+ blHalves: {
68+ parts: ['ablb8', 'ablb6', 'ablb4', 'ablb2'],
69+ displayParts: ['ablb8', 'ablb6', 'ablb4', 'ablb2'],
70+ },
71+ brFulls: {
72+ parts: ['abrf8', 'abrf6', 'abrf4', 'abrf2'],
73+ },
74+ brHalves: {
75+ parts: ['abrb8', 'abrb6', 'abrb4', 'abrb2'],
76+ displayParts: ['abrb8', 'abrb6', 'abrb4', 'abrb2'],
77+ },
78+ },
79+ parts: ['ablf8', 'ablf6', 'ablf4', 'ablf2'],
80+ },
81+ arc: {
82+ modes: {
83+ rings: {
84+ parts: ['am081', 'am082', 'am083', 'am08s'],
85+ },
86+ diagonals: {
87+ parts: ['am741', 'am742', 'am743', 'am422', 'am423'],
88+ },
89+ topsBottoms: {
90+ parts: ['ab2', 'ab3', 'ab4', 'at1', 'at3', 'at4'],
91+ },
92+ leftsRights: {
93+ parts: ['al2', 'al3', 'al4', 'ar1', 'ar2', 'ar4'/*, 'arb2'*/],
94+ },
95+ rights: {
96+ parts: ['am642', 'am643'],
97+ },
98+ middles: {
99+ parts: ['am222', 'am223', 'am622', 'am623'],
100+ },
101+ middless: {
102+ parts: ['am142', 'am443'],
103+ },
104+ topRights: {
105+ parts: ['am021', 'am022', 'am023', 'am242', 'arb2'],
106+ },
107+ },
108+ parts: ['am222', 'am223', 'ar4', 'ar2', 'ar1'],
109+ },
110+ circle: {
111+ modes: {
112+ mediums: {
113+ parts: [
114+ 'cmmb',
115+ 'ctl3mb', 'cbl3mb',
116+ 'ct2mb', 'cb2mb', 'cl2mb', 'cr2mb', 'ctl2mb', 'ctr2mb', 'cbl2mb', 'cbr2mb',
117+ 'ctl1mb', 'ctr1mb', 'cbl1mb', 'cbr1mb',
118+ 'cbl3mf', 'ctr3mf', 'ctl3mf',
119+ ],
120+ displayParts: [
121+ 'cmmb',
122+ 'ctl3mb', 'cbl3mb',
123+ 'ct2mb', 'cb2mb', 'cl2mb', 'cr2mb', 'ctl2mb', 'ctr2mb', 'cbl2mb', 'cbr2mb',
124+ 'ctl1mb', 'ctr1mb', 'cbl1mb', 'cbr1mb',
125+ 'ctr3mf',
126+ ],
127+ },
128+ sides: {
129+ parts: ['cl1mb', 'cr1mb', 'cl3mb', 'cr3mb'],
130+ },
131+ smalls: {
132+ parts: ['cr1sb', 'cr2sb'],
133+ },
134+ },
135+ displayParts: [
136+ 'cbr3mb', 'ctl3mb',
137+ 'cmmb',
138+ 'ct2mb', 'cl2mb', 'cb2mb', 'cr2mb',
139+ ],
140+ },
141+ donut: {
142+ parts: ['dbl', 'dl', 'dm', 'dtl', 'dtr'],
143+ displayParts: ['dbl', /*'dl',*/ 'dm', 'dtl', 'dtr'],
144+ },
145+ bezier: {
146+ parts: ['b1l1', 'b1l2', 'b1l3', 'b4t2', 'b4t3'],
147+ },
148+};
ui/src/types/store.ts+2
@@ -27,4 +27,6 @@ export type SearchedContactsState = Rolodex;
2727export class SearchSettings {
2828 includeMoons: boolean = true;
2929 includeComets: boolean = false;
30+
31+ allowFictionalSigils: boolean = false;
3032}