mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
Five unrelated packages each had a few imports pointing at the wrong
file or missing from their public surface. Grouped because none of
the individual fixes warrants its own commit and they all unblock
the same downstream consumer (apps/mana/apps/web type-check).
packages/help
- HelpPage.svelte: `'../types.js'` and `'./content'` for
HelpPageProps/HelpSection/SearchResult — neither path exists.
Real homes are `../ui-types` (props) and `../search-types`
(search shapes). Fix the imports.
- HelpSearch.svelte: same `'../content'` typo for SearchResult →
`'../search-types'`.
- translations.ts: `'./types.js'` for HelpPageTranslations →
`'./ui-types'`.
- ui-types.ts: was importing SearchResult from `'./content'` but
that module only exports content shapes. Split into two imports
so HelpContent stays from content.ts and SearchResult comes from
search-types.ts.
packages/feedback
- FeedbackPage.svelte: imported `Feedback` and `CreateFeedbackInput`
from `'./createFeedbackService'` but the service module only
exports the service factory. Real homes are `'./feedback'`
(Feedback) and `'./api'` (CreateFeedbackInput).
- FeedbackForm.svelte: same `'./feedback'` typo for
CreateFeedbackInput → `'./api'`.
packages/subscriptions
- UsageCard / CostCard / pages/SubscriptionPage: all imported
UsageData / CostItem from `'./plans'` but those types live in
`'./usage'`. SubscriptionPage additionally had a relative-path
bug — it's at `src/pages/`, not `src/`, so `./plans` resolved
to `pages/plans` (nonexistent). Now imports `'../plans'` for
plan types and `'../usage'` for usage/cost types.
packages/shared-ui
- index.ts: re-exports the QuickInputItem family from
`./quick-input` but had forgotten `HighlightPattern`. Added.
Apps that build their own InputBar pattern config (e.g.
mana/web/src/lib/quick-input/types.ts) need it as a public type.
- PillNavigation.svelte: imported `SpotlightAction` and
`ContentSearcher` from `./GlobalSpotlight.svelte` (a Svelte
component file), which only re-exports the default. Both types
live in `./types`. Move them to the existing types-import
block; the GlobalSpotlight import becomes a plain default.
packages/shared-auth-ui
- stores/createAuthStore.svelte.ts: imported AuthServiceAdapter /
AuthResult / BaseUser from `'./types'` (nonexistent — the file
is `'./store-types'`).
Net: -23 type errors. Zero behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
218 lines
6 KiB
Svelte
218 lines
6 KiB
Svelte
<script lang="ts">
|
|
import type { HelpSearchProps } from '../ui-types';
|
|
import type { SearchResult } from '../search-types';
|
|
import { createSearcher } from '../search-engine';
|
|
|
|
let { content, translations, placeholder, onResultSelect }: HelpSearchProps = $props();
|
|
|
|
let query = $state('');
|
|
let results = $state<SearchResult[]>([]);
|
|
let isSearching = $state(false);
|
|
let showResults = $state(false);
|
|
let selectedIndex = $state(-1);
|
|
|
|
const searcher = $derived(createSearcher(content));
|
|
|
|
let debounceTimer: ReturnType<typeof setTimeout>;
|
|
|
|
function handleInput(event: Event) {
|
|
const target = event.target as HTMLInputElement;
|
|
query = target.value;
|
|
selectedIndex = -1;
|
|
|
|
clearTimeout(debounceTimer);
|
|
|
|
if (query.trim().length < 2) {
|
|
results = [];
|
|
showResults = false;
|
|
return;
|
|
}
|
|
|
|
isSearching = true;
|
|
debounceTimer = setTimeout(() => {
|
|
results = searcher(query, { limit: 8 });
|
|
isSearching = false;
|
|
showResults = true;
|
|
}, 300);
|
|
}
|
|
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
if (!showResults || results.length === 0) return;
|
|
|
|
switch (event.key) {
|
|
case 'ArrowDown':
|
|
event.preventDefault();
|
|
selectedIndex = Math.min(selectedIndex + 1, results.length - 1);
|
|
break;
|
|
case 'ArrowUp':
|
|
event.preventDefault();
|
|
selectedIndex = Math.max(selectedIndex - 1, -1);
|
|
break;
|
|
case 'Enter':
|
|
event.preventDefault();
|
|
if (selectedIndex >= 0 && results[selectedIndex]) {
|
|
selectResult(results[selectedIndex]);
|
|
}
|
|
break;
|
|
case 'Escape':
|
|
showResults = false;
|
|
selectedIndex = -1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
function selectResult(result: SearchResult) {
|
|
onResultSelect(result);
|
|
query = '';
|
|
results = [];
|
|
showResults = false;
|
|
selectedIndex = -1;
|
|
}
|
|
|
|
function handleBlur(event: FocusEvent) {
|
|
// Only close if focus moves outside the search container
|
|
const relatedTarget = event.relatedTarget as HTMLElement | null;
|
|
if (relatedTarget?.closest('[data-help-search]')) return;
|
|
showResults = false;
|
|
}
|
|
|
|
function getTypeIcon(type: string): string {
|
|
switch (type) {
|
|
case 'faq':
|
|
return '?';
|
|
case 'feature':
|
|
return '★';
|
|
case 'guide':
|
|
return '📖';
|
|
case 'changelog':
|
|
return '📋';
|
|
default:
|
|
return '•';
|
|
}
|
|
}
|
|
|
|
function getTypeLabel(type: string): string {
|
|
switch (type) {
|
|
case 'faq':
|
|
return 'FAQ';
|
|
case 'feature':
|
|
return 'Feature';
|
|
case 'guide':
|
|
return 'Guide';
|
|
case 'changelog':
|
|
return 'Changelog';
|
|
default:
|
|
return type;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="relative" data-help-search>
|
|
<div class="relative">
|
|
<input
|
|
type="text"
|
|
value={query}
|
|
oninput={handleInput}
|
|
onkeydown={handleKeyDown}
|
|
onfocus={() => query.length >= 2 && (showResults = true)}
|
|
onblur={handleBlur}
|
|
placeholder={placeholder ?? translations.search.noResults}
|
|
class="w-full rounded-lg border border-gray-300 bg-white py-2.5 pl-10 pr-4 text-sm text-gray-900 placeholder-gray-500 transition-colors focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400"
|
|
aria-label={placeholder ?? translations.search.noResults}
|
|
role="combobox"
|
|
aria-expanded={showResults}
|
|
aria-haspopup="listbox"
|
|
/>
|
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
|
{#if isSearching}
|
|
<svg
|
|
class="h-5 w-5 animate-spin text-gray-400"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
|
|
></circle>
|
|
<path
|
|
class="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
></path>
|
|
</svg>
|
|
{:else}
|
|
<svg
|
|
class="h-5 w-5 text-gray-400"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
{#if showResults}
|
|
<div
|
|
class="absolute z-50 mt-1 w-full rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
|
|
role="listbox"
|
|
>
|
|
{#if results.length === 0}
|
|
<div class="p-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
{translations.search.noResults.replace('{query}', query)}
|
|
</div>
|
|
{:else}
|
|
<ul class="max-h-96 overflow-auto py-2">
|
|
{#each results as result, index (result.id)}
|
|
<li role="option" aria-selected={selectedIndex === index}>
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-start gap-3 px-4 py-2 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-700 {selectedIndex ===
|
|
index
|
|
? 'bg-primary-50 dark:bg-primary-900/20'
|
|
: ''}"
|
|
onmousedown={(e) => {
|
|
e.preventDefault();
|
|
selectResult(result);
|
|
}}
|
|
>
|
|
<span
|
|
class="mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-gray-100 text-xs dark:bg-gray-700"
|
|
aria-label={getTypeLabel(result.type)}
|
|
>
|
|
{getTypeIcon(result.type)}
|
|
</span>
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-center gap-2">
|
|
<span class="truncate font-medium text-gray-900 dark:text-gray-100">
|
|
{@html result.highlight ?? result.title}
|
|
</span>
|
|
<span
|
|
class="flex-shrink-0 rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-400"
|
|
>
|
|
{getTypeLabel(result.type)}
|
|
</span>
|
|
</div>
|
|
<p class="mt-0.5 truncate text-sm text-gray-500 dark:text-gray-400">
|
|
{result.excerpt}
|
|
</p>
|
|
</div>
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<div
|
|
class="border-t border-gray-200 px-4 py-2 text-xs text-gray-500 dark:border-gray-700 dark:text-gray-400"
|
|
>
|
|
{translations.search.resultsCount.replace('{count}', String(results.length))}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|