feat: adapt SearchSidebar to use new sidebar system with component-based content
This commit is contained in:
parent
f5361c5ace
commit
fc711a7a5d
5 changed files with 637 additions and 18 deletions
510
src/lib/components/sidebar/SearchContent.svelte
Normal file
510
src/lib/components/sidebar/SearchContent.svelte
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import { SearchAdapter } from '$lib/api/adapters/search.adapter'
|
||||
import type { SearchResult } from '$lib/api/adapters/search.adapter'
|
||||
import Button from '../ui/Button.svelte'
|
||||
import { getCharacterImage, getWeaponImage, getSummonImage } from '$lib/features/database/detail/image'
|
||||
|
||||
interface Props {
|
||||
type: 'weapon' | 'character' | 'summon'
|
||||
onAddItems?: (items: SearchResult[]) => void
|
||||
canAddMore?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
type = 'weapon',
|
||||
onAddItems = () => {},
|
||||
canAddMore = true
|
||||
}: Props = $props()
|
||||
|
||||
// Search adapter
|
||||
const searchAdapter = new SearchAdapter()
|
||||
|
||||
// Search state
|
||||
let searchQuery = $state('')
|
||||
let searchResults = $state<SearchResult[]>([])
|
||||
let isLoading = $state(false)
|
||||
let currentPage = $state(1)
|
||||
let totalPages = $state(1)
|
||||
let hasInitialLoad = $state(false)
|
||||
|
||||
// Filter state
|
||||
let elementFilters = $state<number[]>([])
|
||||
let rarityFilters = $state<number[]>([])
|
||||
let proficiencyFilters = $state<number[]>([])
|
||||
|
||||
// Refs
|
||||
let searchInput: HTMLInputElement
|
||||
|
||||
// Constants
|
||||
const elements = [
|
||||
{ value: 0, label: 'Null', color: 'var(--grey-50)' },
|
||||
{ value: 1, label: 'Wind', color: 'var(--wind-bg)' },
|
||||
{ value: 2, label: 'Fire', color: 'var(--fire-bg)' },
|
||||
{ value: 3, label: 'Water', color: 'var(--water-bg)' },
|
||||
{ value: 4, label: 'Earth', color: 'var(--earth-bg)' },
|
||||
{ value: 5, label: 'Dark', color: 'var(--dark-bg)' },
|
||||
{ value: 6, label: 'Light', color: 'var(--light-bg)' }
|
||||
]
|
||||
|
||||
const rarities = [
|
||||
{ value: 1, label: 'R' },
|
||||
{ value: 2, label: 'SR' },
|
||||
{ value: 3, label: 'SSR' }
|
||||
]
|
||||
|
||||
const proficiencies = [
|
||||
{ value: 1, label: 'Sabre' },
|
||||
{ value: 2, label: 'Dagger' },
|
||||
{ value: 3, label: 'Spear' },
|
||||
{ value: 4, label: 'Axe' },
|
||||
{ value: 5, label: 'Staff' },
|
||||
{ value: 6, label: 'Gun' },
|
||||
{ value: 7, label: 'Melee' },
|
||||
{ value: 8, label: 'Bow' },
|
||||
{ value: 9, label: 'Harp' },
|
||||
{ value: 10, label: 'Katana' }
|
||||
]
|
||||
|
||||
// Focus search input on mount
|
||||
$effect(() => {
|
||||
if (searchInput) {
|
||||
searchInput.focus()
|
||||
}
|
||||
// Load recent items on mount
|
||||
if (!hasInitialLoad) {
|
||||
hasInitialLoad = true
|
||||
performSearch()
|
||||
}
|
||||
})
|
||||
|
||||
// Search when query or filters change (debounced by adapter)
|
||||
$effect(() => {
|
||||
if (hasInitialLoad) {
|
||||
performSearch()
|
||||
}
|
||||
})
|
||||
|
||||
async function performSearch() {
|
||||
isLoading = true
|
||||
|
||||
try {
|
||||
const params = {
|
||||
query: searchQuery || '',
|
||||
page: currentPage,
|
||||
filters: {
|
||||
element: elementFilters.length > 0 ? elementFilters : undefined,
|
||||
rarity: rarityFilters.length > 0 ? rarityFilters : undefined,
|
||||
proficiency1: type === 'weapon' && proficiencyFilters.length > 0 ? proficiencyFilters : undefined
|
||||
}
|
||||
}
|
||||
|
||||
let response
|
||||
switch (type) {
|
||||
case 'weapon':
|
||||
response = await searchAdapter.searchWeapons(params)
|
||||
break
|
||||
case 'character':
|
||||
response = await searchAdapter.searchCharacters(params)
|
||||
break
|
||||
case 'summon':
|
||||
response = await searchAdapter.searchSummons(params)
|
||||
break
|
||||
}
|
||||
|
||||
if (response) {
|
||||
searchResults = response.results || []
|
||||
totalPages = response.totalPages || 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
searchResults = []
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemClick(item: SearchResult) {
|
||||
if (canAddMore) {
|
||||
onAddItems([item])
|
||||
}
|
||||
}
|
||||
|
||||
function toggleElementFilter(element: number) {
|
||||
if (elementFilters.includes(element)) {
|
||||
elementFilters = elementFilters.filter(e => e !== element)
|
||||
} else {
|
||||
elementFilters = [...elementFilters, element]
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRarityFilter(rarity: number) {
|
||||
if (rarityFilters.includes(rarity)) {
|
||||
rarityFilters = rarityFilters.filter(r => r !== rarity)
|
||||
} else {
|
||||
rarityFilters = [...rarityFilters, rarity]
|
||||
}
|
||||
}
|
||||
|
||||
function toggleProficiencyFilter(prof: number) {
|
||||
if (proficiencyFilters.includes(prof)) {
|
||||
proficiencyFilters = proficiencyFilters.filter(p => p !== prof)
|
||||
} else {
|
||||
proficiencyFilters = [...proficiencyFilters, prof]
|
||||
}
|
||||
}
|
||||
|
||||
function getImageUrl(item: SearchResult): string {
|
||||
const id = item.granblueId
|
||||
if (!id) return `/images/placeholders/placeholder-${type}-square.png`
|
||||
|
||||
switch (type) {
|
||||
case 'character':
|
||||
return getCharacterImage(id, '01', 'square')
|
||||
case 'weapon':
|
||||
return getWeaponImage(id, 'square')
|
||||
case 'summon':
|
||||
return getSummonImage(id, 'square')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getItemName(item: SearchResult): string {
|
||||
const name = item.name
|
||||
if (typeof name === 'string') return name
|
||||
return name?.en || name?.ja || 'Unknown'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="search-content">
|
||||
<div class="search-section">
|
||||
<input
|
||||
bind:this={searchInput}
|
||||
bind:value={searchQuery}
|
||||
type="text"
|
||||
placeholder="Search by name..."
|
||||
aria-label="Search"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<!-- Element filters -->
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">Element</label>
|
||||
<div class="filter-buttons">
|
||||
{#each elements as element}
|
||||
<button
|
||||
class="filter-btn element-btn"
|
||||
class:active={elementFilters.includes(element.value)}
|
||||
style:--element-color={element.color}
|
||||
onclick={() => toggleElementFilter(element.value)}
|
||||
aria-pressed={elementFilters.includes(element.value)}
|
||||
>
|
||||
{element.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rarity filters -->
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">Rarity</label>
|
||||
<div class="filter-buttons">
|
||||
{#each rarities as rarity}
|
||||
<button
|
||||
class="filter-btn rarity-btn"
|
||||
class:active={rarityFilters.includes(rarity.value)}
|
||||
onclick={() => toggleRarityFilter(rarity.value)}
|
||||
aria-pressed={rarityFilters.includes(rarity.value)}
|
||||
>
|
||||
{rarity.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proficiency filters (weapons only) -->
|
||||
{#if type === 'weapon'}
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">Proficiency</label>
|
||||
<div class="filter-buttons proficiency-grid">
|
||||
{#each proficiencies as prof}
|
||||
<button
|
||||
class="filter-btn prof-btn"
|
||||
class:active={proficiencyFilters.includes(prof.value)}
|
||||
onclick={() => toggleProficiencyFilter(prof.value)}
|
||||
aria-pressed={proficiencyFilters.includes(prof.value)}
|
||||
>
|
||||
{prof.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="results-section">
|
||||
{#if isLoading}
|
||||
<div class="loading">Searching...</div>
|
||||
{:else if searchResults.length > 0}
|
||||
<ul class="results-list">
|
||||
{#each searchResults as item (item.id)}
|
||||
<li class="result-item">
|
||||
<button
|
||||
class="result-button"
|
||||
class:disabled={!canAddMore}
|
||||
onclick={() => handleItemClick(item)}
|
||||
aria-label="{canAddMore ? 'Add' : 'Grid full - cannot add'} {getItemName(item)}"
|
||||
disabled={!canAddMore}
|
||||
>
|
||||
<img
|
||||
src={getImageUrl(item)}
|
||||
alt={getItemName(item)}
|
||||
class="result-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span class="result-name">{getItemName(item)}</span>
|
||||
{#if item.element !== undefined}
|
||||
<span
|
||||
class="result-element"
|
||||
style:color={elements.find(e => e.value === item.element)?.color}
|
||||
>
|
||||
{elements.find(e => e.value === item.element)?.label}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="pagination">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onclick={() => currentPage = Math.max(1, currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<span class="page-info">Page {currentPage} of {totalPages}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onclick={() => currentPage = Math.min(totalPages, currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if searchQuery.length > 0}
|
||||
<div class="no-results">No results found</div>
|
||||
{:else}
|
||||
<div class="no-results">Start typing to search</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/layout' as *;
|
||||
|
||||
.search-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 60px); // Account for sidebar header
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 0 0 $unit-2x 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: $unit calc($unit * 1.5);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: $input-corner;
|
||||
font-size: $font-regular;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-blue);
|
||||
box-shadow: 0 0 0 2px var(--accent-blue-alpha);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
padding-bottom: $unit-2x;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
flex-shrink: 0;
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: calc($unit * 1.5);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: block;
|
||||
font-size: $font-tiny;
|
||||
font-weight: $bold;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: $unit;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.proficiency-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: $unit-half $unit;
|
||||
border: 1px solid var(--border-primary);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: $input-corner;
|
||||
font-size: $font-small;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: var(--text-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--border-secondary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--accent-blue);
|
||||
color: white;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
&.element-btn.active {
|
||||
background: var(--element-color);
|
||||
border-color: var(--element-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.results-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $unit-2x 0;
|
||||
min-height: 0;
|
||||
|
||||
.loading,
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: $unit-3x;
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-regular;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
margin-bottom: $unit-half;
|
||||
|
||||
.result-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
padding: $unit;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $input-corner;
|
||||
background: var(--bg-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: var(--bg-disabled);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-disabled);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
flex: 1;
|
||||
font-size: $font-regular;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.result-element {
|
||||
font-size: $font-small;
|
||||
font-weight: $bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: $unit-2x;
|
||||
margin-top: $unit-2x;
|
||||
padding-top: $unit-2x;
|
||||
border-top: 1px solid var(--border-primary);
|
||||
|
||||
.page-info {
|
||||
font-size: $font-small;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
src/lib/features/search/openSearchSidebar.svelte.ts
Normal file
25
src/lib/features/search/openSearchSidebar.svelte.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import SearchContent from '$lib/components/sidebar/SearchContent.svelte'
|
||||
import type { SearchResult } from '$lib/api/adapters/search.adapter'
|
||||
|
||||
interface SearchSidebarOptions {
|
||||
type: 'weapon' | 'character' | 'summon'
|
||||
onAddItems?: (items: SearchResult[]) => void
|
||||
canAddMore?: boolean
|
||||
}
|
||||
|
||||
export function openSearchSidebar(options: SearchSidebarOptions) {
|
||||
const { type, onAddItems, canAddMore = true } = options
|
||||
|
||||
// Open the sidebar with the search component
|
||||
const title = `Search ${type.charAt(0).toUpperCase() + type.slice(1)}s`
|
||||
sidebar.openWithComponent(title, SearchContent, {
|
||||
type,
|
||||
onAddItems,
|
||||
canAddMore
|
||||
})
|
||||
}
|
||||
|
||||
export function closeSearchSidebar() {
|
||||
sidebar.close()
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Snippet } from 'svelte'
|
||||
import type { Snippet, Component } from 'svelte'
|
||||
|
||||
// Standard sidebar width
|
||||
export const SIDEBAR_WIDTH = '420px'
|
||||
|
|
@ -7,19 +7,33 @@ interface SidebarState {
|
|||
open: boolean
|
||||
title?: string
|
||||
content?: Snippet
|
||||
component?: Component
|
||||
componentProps?: Record<string, any>
|
||||
}
|
||||
|
||||
class SidebarStore {
|
||||
state = $state<SidebarState>({
|
||||
open: false,
|
||||
title: undefined,
|
||||
content: undefined
|
||||
content: undefined,
|
||||
component: undefined,
|
||||
componentProps: undefined
|
||||
})
|
||||
|
||||
open(title?: string, content?: Snippet) {
|
||||
this.state.open = true
|
||||
this.state.title = title
|
||||
this.state.content = content
|
||||
this.state.component = undefined
|
||||
this.state.componentProps = undefined
|
||||
}
|
||||
|
||||
openWithComponent(title: string, component: Component, props?: Record<string, any>) {
|
||||
this.state.open = true
|
||||
this.state.title = title
|
||||
this.state.component = component
|
||||
this.state.componentProps = props
|
||||
this.state.content = undefined
|
||||
}
|
||||
|
||||
close() {
|
||||
|
|
@ -28,6 +42,8 @@ class SidebarStore {
|
|||
setTimeout(() => {
|
||||
this.state.title = undefined
|
||||
this.state.content = undefined
|
||||
this.state.component = undefined
|
||||
this.state.componentProps = undefined
|
||||
}, 300)
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +66,14 @@ class SidebarStore {
|
|||
get content() {
|
||||
return this.state.content
|
||||
}
|
||||
|
||||
get component() {
|
||||
return this.state.component
|
||||
}
|
||||
|
||||
get componentProps() {
|
||||
return this.state.componentProps
|
||||
}
|
||||
}
|
||||
|
||||
export const sidebar = new SidebarStore()
|
||||
|
|
@ -32,7 +32,9 @@
|
|||
title={sidebar.title}
|
||||
onclose={() => sidebar.close()}
|
||||
>
|
||||
{#if sidebar.content}
|
||||
{#if sidebar.component}
|
||||
<svelte:component this={sidebar.component} {...sidebar.componentProps} />
|
||||
{:else if sidebar.content}
|
||||
{@render sidebar.content()}
|
||||
{/if}
|
||||
</Sidebar>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,38 @@
|
|||
<script lang="ts">
|
||||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
import { openSearchSidebar } from '$lib/features/search/openSearchSidebar.svelte'
|
||||
import type { SearchResult } from '$lib/api/adapters/search.adapter'
|
||||
|
||||
function openSearchSidebar() {
|
||||
sidebar.open('Search', searchContent)
|
||||
let selectedItems = $state<SearchResult[]>([])
|
||||
|
||||
function handleAddItems(items: SearchResult[]) {
|
||||
selectedItems = [...selectedItems, ...items]
|
||||
console.log('Added items:', items)
|
||||
}
|
||||
|
||||
function openWeaponSearch() {
|
||||
openSearchSidebar({
|
||||
type: 'weapon',
|
||||
onAddItems: handleAddItems,
|
||||
canAddMore: true
|
||||
})
|
||||
}
|
||||
|
||||
function openCharacterSearch() {
|
||||
openSearchSidebar({
|
||||
type: 'character',
|
||||
onAddItems: handleAddItems,
|
||||
canAddMore: true
|
||||
})
|
||||
}
|
||||
|
||||
function openSummonSearch() {
|
||||
openSearchSidebar({
|
||||
type: 'summon',
|
||||
onAddItems: handleAddItems,
|
||||
canAddMore: true
|
||||
})
|
||||
}
|
||||
|
||||
function openDetailsSidebar() {
|
||||
|
|
@ -22,8 +51,14 @@
|
|||
<p>Click the buttons below to test different sidebar configurations:</p>
|
||||
|
||||
<div class="button-group">
|
||||
<Button variant="primary" onclick={openSearchSidebar}>
|
||||
Open Search Sidebar
|
||||
<Button variant="primary" onclick={openWeaponSearch}>
|
||||
Search Weapons
|
||||
</Button>
|
||||
<Button variant="primary" onclick={openCharacterSearch}>
|
||||
Search Characters
|
||||
</Button>
|
||||
<Button variant="primary" onclick={openSummonSearch}>
|
||||
Search Summons
|
||||
</Button>
|
||||
<Button variant="secondary" onclick={openDetailsSidebar}>
|
||||
Open Details Sidebar
|
||||
|
|
@ -33,6 +68,17 @@
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
{#if selectedItems.length > 0}
|
||||
<div class="selected-items">
|
||||
<h3>Selected Items ({selectedItems.length})</h3>
|
||||
<ul>
|
||||
{#each selectedItems as item}
|
||||
<li>{item.name?.en || item.name?.ja || item.name || 'Unknown'}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="content">
|
||||
<h2>Main Content Area</h2>
|
||||
<p>This content will shrink when the sidebar opens, creating a two-pane layout.</p>
|
||||
|
|
@ -41,17 +87,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#snippet searchContent()}
|
||||
<div class="sidebar-demo-content">
|
||||
<input type="text" placeholder="Search..." class="search-input" />
|
||||
<div class="search-results">
|
||||
<div class="result-item">Result 1</div>
|
||||
<div class="result-item">Result 2</div>
|
||||
<div class="result-item">Result 3</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet detailsContent()}
|
||||
<div class="sidebar-demo-content">
|
||||
<h3>Item Name</h3>
|
||||
|
|
@ -110,6 +145,29 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.selected-items {
|
||||
margin: $unit-2x 0;
|
||||
padding: $unit-2x;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 $unit 0;
|
||||
font-size: $font-medium;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: $unit-2x;
|
||||
|
||||
li {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: $unit-half;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: $unit-3x;
|
||||
padding: $unit-2x;
|
||||
|
|
|
|||
Loading…
Reference in a new issue