Graph calculator improvmentes
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 24s

This commit is contained in:
2026-10-02 14:53:00 +02:00
parent e36fef7290
commit 68b74eb60b
21 changed files with 1092 additions and 392 deletions

View File

@@ -1,9 +1,13 @@
<script setup lang="ts">
import type { ArticleMeta } from './ArticleCard.vue'
import type { ToolPage } from '~/utils/routes'
/* Centered search popup: the sidebar and home search bars (or Ctrl/⌘+K,
or "/" anywhere but a text field) open it, then it live-searches
articles against the API as you type. ↑/↓ select, ↵ opens, esc closes. */
articles against the API as you type. ↑/↓ select, ↵ opens, esc closes.
The wiki's own pages — the link map and the graphing calculator — answer
the search too, and ↵ on one lays that surface on the book beside the
open articles rather than turning the page away to it. */
const api = `${useRuntimeConfig().public.apiBase}/api`
const route = useRoute()
const router = useRouter()
@@ -13,9 +17,20 @@ const { open, show, hide, toggle } = useSearch()
const { hide: hideSettings } = useSettings()
const { hide: hideProfile } = useProfileMenu()
const { viewed } = useRecentlyViewed()
const { openTool } = useArticleTabs()
const MAX_RESULTS = 8
/* the wiki's own pages join the search under their names and a few plain
words for what they are; each answers at its own address */
const PAGE_TOOLS: { tool: ToolPage; terms: string[] }[] = [
{ tool: 'graph', terms: ['graph', 'map', 'link map', 'network', 'constellation'] },
{
tool: 'calculator',
terms: ['calculator', 'calc', 'plot', 'plotter', 'graphing', 'plane'],
},
]
const q = ref('')
const results = ref<ArticleMeta[]>([])
const loading = ref(false)
@@ -24,6 +39,17 @@ const activeIndex = ref(0)
const trimmed = computed(() => q.value.trim())
/* which of the wiki's own pages the term names — matched against the words
and the page's own name, either reading into the other, so "calc" leads
to the Calculator as surely as the whole word does */
const pageRows = computed(() => {
const term = trimmed.value.toLowerCase()
if (!term) return []
return PAGE_TOOLS.filter(({ tool, terms }) =>
terms.some((w) => w.includes(term) || term.includes(w)) || tool.includes(term)
)
})
/* --- focus: the field takes focus on open, focus returns on close --- */
const inputEl = ref<HTMLInputElement | null>(null)
let previouslyFocused: HTMLElement | null = null
@@ -122,8 +148,19 @@ function openAllResults() {
void router.push({ path: '/', query: { q: trimmed.value } })
}
/* open one of the wiki's own pages: lay the page on the book first, so it
arrives with the surface already holding it, then turn the desk to its
address — where the page is already open, this simply turns back to it */
function openPageTool(tool: ToolPage) {
hide()
openTool(tool)
void router.push(toolHref(tool))
}
/* ↑/↓ walk the page rows and the articles as one list — the wiki's own
pages lead when they match */
function onFieldKeydown(e: KeyboardEvent) {
const n = results.value.length
const n = pageRows.value.length + results.value.length
if (e.key === 'ArrowDown' && n) {
e.preventDefault()
activeIndex.value = (activeIndex.value + 1) % n
@@ -132,7 +169,12 @@ function onFieldKeydown(e: KeyboardEvent) {
activeIndex.value = (activeIndex.value - 1 + n) % n
} else if (e.key === 'Enter') {
e.preventDefault()
const chosen = results.value[activeIndex.value] ?? results.value[0]
const page = pageRows.value[activeIndex.value]
if (page) {
openPageTool(page.tool)
return
}
const chosen = results.value[activeIndex.value - pageRows.value.length] ?? results.value[0]
if (chosen) openArticle(chosen)
else if (trimmed.value) openAllResults()
}
@@ -209,18 +251,52 @@ function highlight(text: string): Part[] {
</div>
<div class="search-modal-body">
<div v-if="failed" class="banner">
<span class="glyph">!</span>
<span>{{ t('search.apiDown') }}</span>
</div>
<ul v-else-if="results.length" class="search-results" role="listbox">
<li v-for="(a, i) in results" :key="a._id" role="option" :aria-selected="i === activeIndex">
<!-- one list, arrowed as one: the wiki's own pages lead when the
term names them, the articles answer under them -->
<ul v-if="pageRows.length || results.length" class="search-results" role="listbox">
<li v-for="(p, i) in pageRows" :key="p.tool" role="option" :aria-selected="i === activeIndex">
<NuxtLink
:to="articleHref(a)"
:to="toolHref(p.tool)"
class="search-result"
:class="{ active: i === activeIndex }"
@mouseenter="activeIndex = i"
@click.prevent="openPageTool(p.tool)"
>
<span class="sr-title">
<span class="sr-page-glyph" aria-hidden="true">
<svg v-if="p.tool === 'graph'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="18" cy="5" r="3" />
<circle cx="6" cy="12" r="3" />
<circle cx="18" cy="19" r="3" />
<path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4" />
</svg>
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 3v17a1 1 0 0 0 1 1h17" />
<path d="M4.5 16.5c2.6 0 3.4-10.5 6.8-10.5s3.9 7 8.2 7" />
</svg>
</span>
<template v-for="(p2, pi) in highlight(t(TOOL_LABEL_KEYS[p.tool]))" :key="pi">
<mark v-if="p2.hit">{{ p2.text }}</mark>
<template v-else>{{ p2.text }}</template>
</template>
</span>
<span class="sr-meta">
<span>{{ toolHref(p.tool) }}</span>
</span>
</NuxtLink>
</li>
<li
v-for="(a, i) in results"
:key="a._id"
role="option"
:aria-selected="i + pageRows.length === activeIndex"
>
<NuxtLink
:to="articleHref(a)"
class="search-result"
:class="{ active: i + pageRows.length === activeIndex }"
@mouseenter="activeIndex = i + pageRows.length"
@click.prevent="openArticle(a)"
>
<span class="sr-title" :lang="articleLanguage(a)">
@@ -245,6 +321,11 @@ function highlight(text: string): Part[] {
</li>
</ul>
<div v-else-if="failed" class="banner">
<span class="glyph">!</span>
<span>{{ t('search.apiDown') }}</span>
</div>
<div v-else-if="loading">
<div v-for="i in 3" :key="i" class="search-skeleton" />
</div>