zenno: format

This commit is contained in:
2026-06-16 14:20:03 -04:00
parent 1d65a0ec14
commit 67377c4690
3 changed files with 71 additions and 59 deletions
+27 -15
View File
@@ -22,7 +22,7 @@
let search = $state(''); let search = $state('');
const charas = $derived(characters ?? []); const charas = $derived(characters ?? []);
const searchedCharas = $derived(stringsearch(search, charas, ({name}) => name) || charas); const searchedCharas = $derived(stringsearch(search, charas, ({ name }) => name) || charas);
$effect(() => { $effect(() => {
if (required && value == null && charas.length > 0) { if (required && value == null && charas.length > 0) {
@@ -41,14 +41,14 @@
case 'ArrowDown': { case 'ArrowDown': {
const opts = [...optionsContainer!.children] as HTMLElement[]; const opts = [...optionsContainer!.children] as HTMLElement[];
const i = opts.findIndex((n) => parseInt(n.dataset.charaId ?? '') === value?.chara_id); const i = opts.findIndex((n) => parseInt(n.dataset.charaId ?? '') === value?.chara_id);
const o = i != null && i >= 0 ? opts[Math.min(i+1, opts.length - 1)] : opts[0]; const o = i != null && i >= 0 ? opts[Math.min(i + 1, opts.length - 1)] : opts[0];
setByElem(o); setByElem(o);
break; break;
} }
case 'ArrowUp': { case 'ArrowUp': {
const opts = [...optionsContainer!.children] as HTMLElement[]; const opts = [...optionsContainer!.children] as HTMLElement[];
const i = opts.findIndex((n) => parseInt(n.dataset.charaId ?? '') === value?.chara_id); const i = opts.findIndex((n) => parseInt(n.dataset.charaId ?? '') === value?.chara_id);
const o = i != null && i > 0 ? opts[i-1] : opts[0]; const o = i != null && i > 0 ? opts[i - 1] : opts[0];
setByElem(o); setByElem(o);
break; break;
} }
@@ -83,48 +83,60 @@
<div class={className} style={`anchor-name: --anchor-${id}`}> <div class={className} style={`anchor-name: --anchor-${id}`}>
<span <span
{id} {id}
class="block h-9 content-center hover:cursor-pointer bg-mist-300 dark:bg-mist-900" class="block h-9 content-center bg-mist-300 hover:cursor-pointer dark:bg-mist-900"
role="combobox" role="combobox"
aria-expanded={expanded} aria-expanded={expanded}
aria-controls="charaOptions" aria-controls="charaOptions"
aria-haspopup="listbox" aria-haspopup="listbox"
onclick={() => popover?.togglePopover()} onclick={() => popover?.togglePopover()}
{onkeydown} {onkeydown}
tabindex="0" tabindex="0">{value?.name ?? ''}</span
>{value?.name ?? ''}</span> >
<div <div
class="absolute top-2 shadow-lg open:flex flex-col px-2 skill-tip" class="skill-tip absolute top-2 flex-col px-2 shadow-lg open:flex"
style={`position-anchor: --anchor-${id}; position-area: bottom;`} style={`position-anchor: --anchor-${id}; position-area: bottom;`}
id="charaOptions" id="charaOptions"
role="listbox" role="listbox"
popover popover
bind:this={popover} bind:this={popover}
> >
<input class="my-2 border rounded-md min-h-8 pointer-coarse:min-h-12" placeholder=" Search" role="searchbox" bind:value={search} /> <input
class="my-2 min-h-8 rounded-md border pointer-coarse:min-h-12"
placeholder=" Search"
role="searchbox"
bind:value={search}
/>
<div class="max-h-72 overflow-y-scroll" bind:this={optionsContainer}> <div class="max-h-72 overflow-y-scroll" bind:this={optionsContainer}>
{#if !required} {#if !required}
<div <div
class="w-full h-8 hover:cursor-pointer hover:bg-mist-300 hover:dark:bg-mist-900 text-lg" class="h-8 w-full text-lg hover:cursor-pointer hover:bg-mist-300 hover:dark:bg-mist-900"
role="option" role="option"
aria-selected={value == undefined} aria-selected={value == undefined}
tabindex="0" tabindex="0"
data-chara-id="" data-chara-id=""
onmousedown={() => {value = undefined; search = ''; popover!.hidePopover()}} onmousedown={() => {
onfocus={() => value = undefined} value = undefined;
search = '';
popover!.hidePopover();
}}
onfocus={() => (value = undefined)}
{onkeydown} {onkeydown}
> >
<span class="italic text-sm">Reset</span> <span class="text-sm italic">Reset</span>
</div> </div>
{/if} {/if}
{#each searchedCharas as c (c.chara_id)} {#each searchedCharas as c (c.chara_id)}
<div <div
class="w-full h-8 hover:cursor-pointer hover:bg-mist-300 hover:dark:bg-mist-900 text-lg" class="h-8 w-full text-lg hover:cursor-pointer hover:bg-mist-300 hover:dark:bg-mist-900"
role="option" role="option"
aria-selected={value?.chara_id === c.chara_id} aria-selected={value?.chara_id === c.chara_id}
tabindex="0" tabindex="0"
data-chara-id={c.chara_id} data-chara-id={c.chara_id}
onmousedown={() => {value = c; popover!.hidePopover()}} onmousedown={() => {
onfocus={() => value = c} value = c;
popover!.hidePopover();
}}
onfocus={() => (value = c)}
{onkeydown} {onkeydown}
> >
{#if option != null} {#if option != null}
+42 -42
View File
@@ -1,39 +1,39 @@
const WORD_BOUNDARY = " ,!?/-+();#○☆♡'=♪∀゚∴"; const WORD_BOUNDARY = " ,!?/-+();#○☆♡'=♪∀゚∴";
function score(s: string, tt: string): number { function score(s: string, tt: string): number {
let k: number | undefined; let k: number | undefined;
let r = 0; let r = 0;
let run = 0; let run = 0;
for (const c of s) { for (const c of s) {
const j = tt.indexOf(c, k); const j = tt.indexOf(c, k);
// If the character isn't in the string, there's a major penalty. // If the character isn't in the string, there's a major penalty.
if (j < 0) { if (j < 0) {
// The penalty scales with run length, on the assumption that we're // The penalty scales with run length, on the assumption that we're
// typing something else. // typing something else.
// Really this should scale with the longest current run among all // Really this should scale with the longest current run among all
// search terms, but that's infeasible to implement. // search terms, but that's infeasible to implement.
r -= 6 + run*run; r -= 6 + run * run;
run = 0; run = 0;
continue; continue;
} }
run++; run++;
// Characters at word boundaries get extra score. // Characters at word boundaries get extra score.
if (j == 0 || WORD_BOUNDARY.includes(tt[j-1])) { if (j == 0 || WORD_BOUNDARY.includes(tt[j - 1])) {
r += 2; r += 2;
} }
// As do characters that *are* word boundaries. // As do characters that *are* word boundaries.
if (WORD_BOUNDARY.includes(c)) { if (WORD_BOUNDARY.includes(c)) {
r += 2; r += 2;
} }
// And runs of matches scale with run length. // And runs of matches scale with run length.
if (j === k) { if (j === k) {
r += (run+1) * (run+1); r += (run + 1) * (run + 1);
} else { } else {
run = 0; run = 0;
} }
k = j + 1; k = j + 1;
} }
return r; return r;
} }
/** /**
@@ -44,13 +44,13 @@ function score(s: string, tt: string): number {
* @returns Matching terms in decreasing match quality order * @returns Matching terms in decreasing match quality order
*/ */
export function stringsearch<T>(sub: string, terms: Iterable<T>, map: (t: T) => string): T[] { export function stringsearch<T>(sub: string, terms: Iterable<T>, map: (t: T) => string): T[] {
const s = sub.toLocaleLowerCase(); const s = sub.toLocaleLowerCase();
const scored: [T, number][] = []; const scored: [T, number][] = [];
for (const t of terms) { for (const t of terms) {
const sc = score(s, map(t).toLocaleLowerCase()); const sc = score(s, map(t).toLocaleLowerCase());
if (sc >= 0) { if (sc >= 0) {
scored.push([t, sc]); scored.push([t, sc]);
} }
} }
return scored.sort(([, a], [, b]) => b - a).map(([t,]) => t); return scored.sort(([, a], [, b]) => b - a).map(([t]) => t);
} }
+2 -2
View File
@@ -252,7 +252,7 @@
<Sec h={3} id="hp">HP</Sec> <Sec h={3} id="hp">HP</Sec>
<p>Max HP, i.e. starting HP.</p> <p>Max HP, i.e. starting HP.</p>
{@render statChart(race.Stat.Stamina, hp, 'HP', [1000, 5000], {len: true})} {@render statChart(race.Stat.Stamina, hp, 'HP', [1000, 5000], { len: true })}
<Sec h={2} id="power">Power</Sec> <Sec h={2} id="power">Power</Sec>
@@ -270,7 +270,7 @@
<Sec h={3} id="acceleration">Acceleration</Sec> <Sec h={3} id="acceleration">Acceleration</Sec>
<p>Acceleration.</p> <p>Acceleration.</p>
{@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.5], {style: true})} {@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.5], { style: true })}
<Sec h={3} id="lane-change-target-speed">Lane Change Target Speed</Sec> <Sec h={3} id="lane-change-target-speed">Lane Change Target Speed</Sec>
<p>Horizontal (rather than forward) target speed of changing lanes.</p> <p>Horizontal (rather than forward) target speed of changing lanes.</p>