feat: refine database discovery and ISAC interactions
This commit is contained in:
@@ -3,10 +3,10 @@
|
||||
let { active = '' }: { active?: string } = $props();
|
||||
</script>
|
||||
|
||||
<nav class="database-category-nav" aria-label="Game database categories">
|
||||
<nav class="database-category-nav" aria-label="The Division Resurgence database categories">
|
||||
{#each databaseCategories as category}
|
||||
<a class:active={active === category.slug} aria-current={active === category.slug ? 'page' : undefined} href={`/database/${category.slug}`}>
|
||||
<small>{category.code}</small><span>{category.label}</span><b>›</b>
|
||||
<span>{category.label}</span><b>›</b>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import DatabaseFacetFilters from './DatabaseFacetFilters.svelte';
|
||||
import RegistryPagination from './RegistryPagination.svelte';
|
||||
|
||||
let {
|
||||
filters = [],
|
||||
pagination,
|
||||
search,
|
||||
searchPlaceholder = 'SEARCH BY NAME'
|
||||
}: {
|
||||
filters?: any[];
|
||||
pagination: { page: number; perPage: number; total: number; pageCount: number };
|
||||
search: string;
|
||||
searchPlaceholder?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="database-discovery-console" aria-label="Database search, filters, and pagination">
|
||||
{#if filters.length}<DatabaseFacetFilters groups={filters}/>{/if}
|
||||
<RegistryPagination {pagination} {search} {searchPlaceholder}/>
|
||||
</section>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import GameIcon from './GameIcon.svelte';
|
||||
import ItemIcon from './ItemIcon.svelte';
|
||||
import type { GameIconName } from '$lib/game-icons';
|
||||
@@ -9,6 +10,9 @@
|
||||
type Group = { key: string; label: string; kind: 'segmented' | 'searchable'; options: Option[]; selected: string[] };
|
||||
let { groups }: { groups: Group[] } = $props();
|
||||
let queries = $state<Record<string, string>>({});
|
||||
let expanded = $state(true);
|
||||
let observedSelectedCount = 0;
|
||||
const disclosureKey = 'shd.database.filters.expanded';
|
||||
|
||||
const change = (key: string, value: string) => {
|
||||
const params = new URLSearchParams(page.url.searchParams);
|
||||
@@ -32,11 +36,33 @@
|
||||
|| option.searchText?.toLocaleLowerCase().includes(query));
|
||||
};
|
||||
const selectedCount = $derived(groups.reduce((total, group) => total + group.selected.length, 0));
|
||||
$effect(() => {
|
||||
if (selectedCount > observedSelectedCount) expanded = true;
|
||||
observedSelectedCount = selectedCount;
|
||||
});
|
||||
const toggleExpanded = () => {
|
||||
expanded = !expanded;
|
||||
sessionStorage.setItem(disclosureKey, expanded ? 'true' : 'false');
|
||||
};
|
||||
onMount(() => {
|
||||
if (selectedCount) {
|
||||
expanded = true;
|
||||
return;
|
||||
}
|
||||
const stored = sessionStorage.getItem(disclosureKey);
|
||||
expanded = stored === null ? !window.matchMedia('(max-width: 700px)').matches : stored === 'true';
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="database-facet-panel" aria-label="Database filters">
|
||||
<header><div><small>TACTICAL FILTER MATRIX</small><strong>MATCH ANY WITHIN A GROUP · MATCH EVERY ACTIVE GROUP</strong></div>{#if selectedCount}<button type="button" onclick={clear}>CLEAR {selectedCount} FILTER{selectedCount === 1 ? '' : 'S'}</button>{/if}</header>
|
||||
<div class:compact={groups.length === 1} class="database-facet-groups">
|
||||
<section class:collapsed={!expanded} class="database-facet-panel" aria-label="Database filters">
|
||||
<header>
|
||||
<div><small>TACTICAL FILTER MATRIX</small><strong>{selectedCount ? `${selectedCount} ACTIVE · MATCH EVERY ACTIVE GROUP` : 'MATCH ANY WITHIN A GROUP · MATCH EVERY ACTIVE GROUP'}</strong></div>
|
||||
<div class="database-facet-actions">
|
||||
{#if selectedCount}<button type="button" onclick={clear}>CLEAR {selectedCount}</button>{/if}
|
||||
<button type="button" class="database-facet-toggle" aria-expanded={expanded} aria-controls="database-facet-groups" onclick={toggleExpanded}>{expanded ? 'HIDE FILTERS' : `SHOW FILTERS${selectedCount ? ` · ${selectedCount} ACTIVE` : ''}`} <b aria-hidden="true">{expanded ? '−' : '+'}</b></button>
|
||||
</div>
|
||||
</header>
|
||||
<div id="database-facet-groups" hidden={!expanded} class:compact={groups.length === 1} class="database-facet-groups">
|
||||
{#each groups as group}
|
||||
<fieldset class:searchable={group.kind === 'searchable'} class="database-facet-group">
|
||||
<legend>{group.label}<span>{group.selected.length ? `${group.selected.length} ACTIVE` : 'ANY'}</span></legend>
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
|
||||
<nav class={`module-kpis kpi-filters ${className}`} aria-label="Registry filters">
|
||||
{#each items as item}
|
||||
<a href={href(item.value)} class:active={active === item.value} aria-current={active === item.value ? 'true' : undefined}>
|
||||
<span>{item.label}</span><strong>{item.count}</strong><small>{item.description}</small>
|
||||
{@const selected = active === item.value}
|
||||
<a href={href(item.value)} class:active={selected} aria-current={selected ? 'page' : undefined} aria-label={`${item.label}: ${item.count}. ${item.description}. ${selected ? 'Current filter' : 'Apply filter'}`}>
|
||||
<span>{item.label}</span><i aria-hidden="true">{selected ? 'SELECTED' : 'FILTER →'}</i><strong>{item.count}</strong><small>{item.description}</small>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<form class="pagination-search" role="search" onsubmit={(event) => { event.preventDefault(); submitSearch(); }}>
|
||||
{#each retainedParams as [key, value]}<input type="hidden" name={key} {value}/>{/each}
|
||||
<input type="search" name={searchKey} value={searchValue} maxlength="80" placeholder={searchPlaceholder} aria-label={`${searchPlaceholder} — results update as you type`} oninput={(event) => scheduleSearch(event.currentTarget.value)}/>
|
||||
<button class="secondary" aria-busy={searchPending} disabled={searchPending}>{searchPending ? 'SEARCHING…' : 'SEARCH'}</button>
|
||||
<span class="pagination-search-state" class:pending={searchPending} aria-live="polite">{searchPending ? 'SEARCHING…' : 'LIVE SEARCH'}</span>
|
||||
{#if search}<a href={clearSearchHref}>CLEAR</a>{/if}
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export const databaseCategories = [
|
||||
{ slug: 'gear', label: 'Gear', code: 'GEAR', description: 'Equipment slots, base health and armor, tiers, Brand Sets, and talents.' },
|
||||
{ slug: 'weapons', label: 'Weapons', code: 'WPN', description: 'Weapon families, damage types, tier ranges, handling, talents, and bonuses.' },
|
||||
{ slug: 'weapon-mods', label: 'Weapon Mods', code: 'WMOD', description: 'Slot and weapon compatibility, quality rolls, secondary pools, and Mod Effects.' },
|
||||
{ slug: 'gear-mods', label: 'Gear Mods', code: 'GMOD', description: 'Quality-scaled primary rolls and selectable secondary attribute pools.' },
|
||||
{ slug: 'os-protocols', label: 'OS Protocols', code: 'OSPR', description: 'Quality, core profile, primary effect, and unique upgradeable talent.' },
|
||||
{ slug: 'talents', label: 'Talents', code: 'TLNT', description: 'Equipment and weapon talents with tiered descriptions and combat mechanics.' },
|
||||
{ slug: 'brands', label: 'Brands', code: 'BRND', description: 'Brand Set identities and their two-, three-, and four-piece bonuses.' },
|
||||
{ slug: 'skill-chips', label: 'Skill Chips', code: 'CHIP', description: 'Set bonuses, primary and secondary pools, and quality-scaled unique talents.' }
|
||||
{ slug: 'gear', label: 'Gear', description: 'Equipment slots, base health and armor, tiers, Brand Sets, and talents.' },
|
||||
{ slug: 'weapons', label: 'Weapons', description: 'Weapon families, damage types, tier ranges, handling, talents, and bonuses.' },
|
||||
{ slug: 'weapon-mods', label: 'Weapon Mods', description: 'Slot and weapon compatibility, quality rolls, secondary pools, and Mod Effects.' },
|
||||
{ slug: 'gear-mods', label: 'Gear Mods', description: 'Quality-scaled primary rolls and selectable secondary attribute pools.' },
|
||||
{ slug: 'os-protocols', label: 'OS Protocols', description: 'Quality, core profile, primary effect, and unique upgradeable talent.' },
|
||||
{ slug: 'talents', label: 'Talents', description: 'Equipment and weapon talents with tiered descriptions and combat mechanics.' },
|
||||
{ slug: 'brands', label: 'Brands', description: 'Brand Set identities and their two-, three-, and four-piece bonuses.' },
|
||||
{ slug: 'skill-chips', label: 'Skill Chips', description: 'Set bonuses, primary and secondary pools, and quality-scaled unique talents.' }
|
||||
] as const;
|
||||
|
||||
export type DatabaseCategorySlug = typeof databaseCategories[number]['slug'];
|
||||
|
||||
Reference in New Issue
Block a user