mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat(mana/web): app picker — autofocused search + alphabetical order
The "App hinzufügen" picker now sorts available apps alphabetically by their (i18n-resolved) display name and shows an autofocused search input above the list to filter quickly. Enter selects the first match. PickerOverlay gains a `subheader` snippet slot so other pickers can embed their own controls between header and list without having to re-implement the shell. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
41c705a303
commit
ab0ca99239
2 changed files with 81 additions and 3 deletions
|
|
@ -20,6 +20,9 @@
|
|||
items: TItem[];
|
||||
onClose: () => void;
|
||||
item: Snippet<[TItem, number]>;
|
||||
/** Optional snippet rendered between the header and the list
|
||||
* (e.g. a search input). */
|
||||
subheader?: Snippet;
|
||||
/** Optional snippet rendered after the items (e.g. "create custom" button). */
|
||||
footer?: Snippet;
|
||||
emptyLabel?: string;
|
||||
|
|
@ -32,6 +35,7 @@
|
|||
items,
|
||||
onClose,
|
||||
item,
|
||||
subheader,
|
||||
footer,
|
||||
emptyLabel = 'Keine Einträge',
|
||||
width = '320px',
|
||||
|
|
@ -45,6 +49,11 @@
|
|||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{#if subheader}
|
||||
<div class="picker-subheader">
|
||||
{@render subheader()}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="picker-list">
|
||||
{#each items as entry, i}
|
||||
{#if i > 0}<div class="divider"></div>{/if}
|
||||
|
|
@ -116,6 +125,11 @@
|
|||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.picker-subheader {
|
||||
padding: 0 1rem 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.picker-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { tick } from 'svelte';
|
||||
import { MagnifyingGlass } from '@mana/shared-icons';
|
||||
import PickerOverlay from '$lib/components/PickerOverlay.svelte';
|
||||
import { getAccessibleApps } from '$lib/app-registry';
|
||||
|
||||
|
|
@ -23,12 +25,35 @@
|
|||
|
||||
let { onSelect, onClose, activeAppIds = [], userTier = null }: Props = $props();
|
||||
|
||||
let query = $state('');
|
||||
let searchInput = $state<HTMLInputElement | null>(null);
|
||||
|
||||
// Filter twice: tier-gate first (so guests + public users don't see
|
||||
// founder/alpha/beta apps at all), then drop apps that are already
|
||||
// open in the current scene.
|
||||
// open in the current scene. Sort alphabetically by the displayed
|
||||
// (i18n-resolved) name, then apply the search query.
|
||||
let availableApps = $derived(
|
||||
getAccessibleApps(userTier).filter((app) => !activeAppIds.includes(app.id))
|
||||
getAccessibleApps(userTier)
|
||||
.filter((app) => !activeAppIds.includes(app.id))
|
||||
.map((app) => ({ app, displayName: appName(app.id, app.name) }))
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName, 'de'))
|
||||
.filter(({ displayName }) =>
|
||||
query.trim() === '' ? true : displayName.toLowerCase().includes(query.trim().toLowerCase())
|
||||
)
|
||||
.map(({ app }) => app)
|
||||
);
|
||||
|
||||
// Auto-focus the search input when the picker opens.
|
||||
$effect(() => {
|
||||
tick().then(() => searchInput?.focus());
|
||||
});
|
||||
|
||||
function handleSearchKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && availableApps.length > 0) {
|
||||
e.preventDefault();
|
||||
onSelect(availableApps[0].id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<PickerOverlay
|
||||
|
|
@ -36,8 +61,21 @@
|
|||
items={availableApps}
|
||||
{onClose}
|
||||
width="300px"
|
||||
emptyLabel="Alle Apps sind bereits geöffnet"
|
||||
emptyLabel={query.trim() === '' ? 'Alle Apps sind bereits geöffnet' : 'Keine Treffer'}
|
||||
>
|
||||
{#snippet subheader()}
|
||||
<div class="search-wrap">
|
||||
<MagnifyingGlass size={14} />
|
||||
<input
|
||||
bind:this={searchInput}
|
||||
bind:value={query}
|
||||
type="text"
|
||||
placeholder="Suchen…"
|
||||
class="search-input"
|
||||
onkeydown={handleSearchKeydown}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet item(app)}
|
||||
<button class="picker-option" onclick={() => onSelect(app.id)}>
|
||||
<div class="app-dot" style="background-color: {app.color}"></div>
|
||||
|
|
@ -47,6 +85,32 @@
|
|||
</PickerOverlay>
|
||||
|
||||
<style>
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--color-muted) / 0.6);
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.search-wrap:focus-within {
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.search-input::placeholder {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
:global(.picker .app-dot) {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue