5 changed files with 26 additions and 710 deletions
@ -1,672 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { nostrClient } from '../../services/nostr/nostr-client.js'; |
|
||||||
import { relayManager } from '../../services/nostr/relay-manager.js'; |
|
||||||
import { nip19 } from 'nostr-tools'; |
|
||||||
import { KIND_LOOKUP } from '../../types/kind-lookup.js'; |
|
||||||
import type { NostrEvent } from '../../types/nostr.js'; |
|
||||||
|
|
||||||
interface Props { |
|
||||||
onSearchResults?: (results: { events: NostrEvent[]; profiles: string[]; relays?: string[]; eventRelays?: Map<string, string> }) => void; |
|
||||||
} |
|
||||||
|
|
||||||
let { onSearchResults }: Props = $props(); |
|
||||||
|
|
||||||
// NIP-01 standard filter controls |
|
||||||
let filterIds = $state<string>(''); |
|
||||||
let filterAuthors = $state<string>(''); |
|
||||||
let filterKinds = $state<string>(''); |
|
||||||
let filterE = $state<string>(''); |
|
||||||
let filterP = $state<string>(''); |
|
||||||
let filterA = $state<string>(''); |
|
||||||
let filterQ = $state<string>(''); |
|
||||||
let filterT = $state<string>(''); |
|
||||||
let filterC = $state<string>(''); |
|
||||||
let filterD = $state<string>(''); |
|
||||||
let filterSince = $state<string>(''); |
|
||||||
let filterUntil = $state<string>(''); |
|
||||||
let filterLimit = $state<number>(100); |
|
||||||
let searching = $state(false); |
|
||||||
let searchResults = $state<{ events: NostrEvent[]; profiles: string[]; relays?: string[] }>({ events: [], profiles: [] }); |
|
||||||
const eventRelayMap = new Map<string, string>(); |
|
||||||
|
|
||||||
// Helper function to decode bech32 to hex |
|
||||||
async function decodeBech32ToHex(bech32: string, type: 'pubkey' | 'event'): Promise<string | null> { |
|
||||||
try { |
|
||||||
if (!/^(npub|nprofile|note|nevent|naddr)1[a-z0-9]+$/i.test(bech32)) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
const decoded = nip19.decode(bech32); |
|
||||||
|
|
||||||
if (type === 'pubkey') { |
|
||||||
if (decoded.type === 'npub') { |
|
||||||
return String(decoded.data).toLowerCase(); |
|
||||||
} else if (decoded.type === 'nprofile') { |
|
||||||
if (decoded.data && typeof decoded.data === 'object' && 'pubkey' in decoded.data) { |
|
||||||
return String(decoded.data.pubkey).toLowerCase(); |
|
||||||
} |
|
||||||
} else if (decoded.type === 'naddr') { |
|
||||||
if (decoded.data && typeof decoded.data === 'object' && 'pubkey' in decoded.data) { |
|
||||||
return String(decoded.data.pubkey).toLowerCase(); |
|
||||||
} |
|
||||||
} |
|
||||||
} else if (type === 'event') { |
|
||||||
if (decoded.type === 'note') { |
|
||||||
return String(decoded.data).toLowerCase(); |
|
||||||
} else if (decoded.type === 'nevent') { |
|
||||||
if (decoded.data && typeof decoded.data === 'object' && 'id' in decoded.data) { |
|
||||||
return String(decoded.data.id).toLowerCase(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
console.debug('Error decoding bech32:', error); |
|
||||||
} |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
// Helper function to parse comma-separated values and validate hex strings |
|
||||||
async function parseHexList(input: string, type: 'pubkey' | 'event' = 'event'): Promise<string[]> { |
|
||||||
const results: string[] = []; |
|
||||||
const parts = input.split(',').map(s => s.trim()).filter(s => s.length > 0); |
|
||||||
|
|
||||||
for (const part of parts) { |
|
||||||
// Check if it's already a hex string |
|
||||||
if (/^[0-9a-f]{64}$/i.test(part)) { |
|
||||||
results.push(part.toLowerCase()); |
|
||||||
} else { |
|
||||||
// Try to decode as bech32 |
|
||||||
const decoded = await decodeBech32ToHex(part, type); |
|
||||||
if (decoded) { |
|
||||||
results.push(decoded); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return results; |
|
||||||
} |
|
||||||
|
|
||||||
// Helper function to parse comma-separated numbers |
|
||||||
function parseNumberList(input: string): number[] { |
|
||||||
return input |
|
||||||
.split(',') |
|
||||||
.map(s => parseInt(s.trim(), 10)) |
|
||||||
.filter(n => !isNaN(n) && n >= 0 && n <= 65535); |
|
||||||
} |
|
||||||
|
|
||||||
// Helper function to parse comma-separated addressable event refs |
|
||||||
async function parseAddressableList(input: string): Promise<string[]> { |
|
||||||
const results: string[] = []; |
|
||||||
const parts = input.split(',').map(s => s.trim()).filter(s => s.length > 0); |
|
||||||
|
|
||||||
for (const part of parts) { |
|
||||||
const match = part.match(/^(\d+):([^:]+)(?::(.+))?$/); |
|
||||||
if (match) { |
|
||||||
const [, kind, pubkeyPart, dTag] = match; |
|
||||||
let hexPubkey: string | null = null; |
|
||||||
|
|
||||||
if (/^[0-9a-f]{64}$/i.test(pubkeyPart)) { |
|
||||||
hexPubkey = pubkeyPart.toLowerCase(); |
|
||||||
} else { |
|
||||||
hexPubkey = await decodeBech32ToHex(pubkeyPart, 'pubkey'); |
|
||||||
} |
|
||||||
|
|
||||||
if (hexPubkey) { |
|
||||||
if (dTag) { |
|
||||||
results.push(`${kind}:${hexPubkey}:${dTag}`); |
|
||||||
} else { |
|
||||||
results.push(`${kind}:${hexPubkey}:`); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return results; |
|
||||||
} |
|
||||||
|
|
||||||
// Helper function to convert date-time string to unix timestamp |
|
||||||
function dateTimeToUnixTimestamp(dateTimeStr: string): number | null { |
|
||||||
if (!dateTimeStr.trim()) return null; |
|
||||||
|
|
||||||
try { |
|
||||||
const date = new Date(dateTimeStr.trim()); |
|
||||||
|
|
||||||
if (isNaN(date.getTime())) { |
|
||||||
const timestamp = parseInt(dateTimeStr.trim(), 10); |
|
||||||
if (!isNaN(timestamp) && timestamp > 0) { |
|
||||||
return timestamp; |
|
||||||
} |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
return Math.floor(date.getTime() / 1000); |
|
||||||
} catch (error) { |
|
||||||
const timestamp = parseInt(dateTimeStr.trim(), 10); |
|
||||||
if (!isNaN(timestamp) && timestamp > 0) { |
|
||||||
return timestamp; |
|
||||||
} |
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Build NIP-01 filter from form inputs |
|
||||||
async function buildFilter(): Promise<any> { |
|
||||||
const filter: any = {}; |
|
||||||
|
|
||||||
if (filterIds.trim()) { |
|
||||||
const ids = await parseHexList(filterIds, 'event'); |
|
||||||
if (ids.length > 0) { |
|
||||||
filter.ids = ids; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterAuthors.trim()) { |
|
||||||
const authors = await parseHexList(filterAuthors, 'pubkey'); |
|
||||||
if (authors.length > 0) { |
|
||||||
filter.authors = authors; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterKinds.trim()) { |
|
||||||
const kinds = parseNumberList(filterKinds); |
|
||||||
if (kinds.length > 0) { |
|
||||||
filter.kinds = kinds; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterE.trim()) { |
|
||||||
const eTags = await parseHexList(filterE, 'event'); |
|
||||||
if (eTags.length > 0) { |
|
||||||
filter['#e'] = eTags; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterP.trim()) { |
|
||||||
const pTags = await parseHexList(filterP, 'pubkey'); |
|
||||||
if (pTags.length > 0) { |
|
||||||
filter['#p'] = pTags; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterA.trim()) { |
|
||||||
const aTags = await parseAddressableList(filterA); |
|
||||||
if (aTags.length > 0) { |
|
||||||
filter['#a'] = aTags; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterQ.trim()) { |
|
||||||
const qTags = await parseHexList(filterQ, 'event'); |
|
||||||
if (qTags.length > 0) { |
|
||||||
filter['#q'] = qTags; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterT.trim()) { |
|
||||||
const tTags = filterT |
|
||||||
.split(',') |
|
||||||
.map(s => s.trim()) |
|
||||||
.filter(s => s.length > 0); |
|
||||||
if (tTags.length > 0) { |
|
||||||
filter['#T'] = tTags; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterC.trim()) { |
|
||||||
const cTags = filterC |
|
||||||
.split(',') |
|
||||||
.map(s => s.trim()) |
|
||||||
.filter(s => s.length > 0); |
|
||||||
if (cTags.length > 0) { |
|
||||||
filter['#C'] = cTags; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterD.trim()) { |
|
||||||
const dTags = filterD |
|
||||||
.split(',') |
|
||||||
.map(s => s.trim()) |
|
||||||
.filter(s => s.length > 0); |
|
||||||
if (dTags.length > 0) { |
|
||||||
filter['#d'] = dTags; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterSince.trim()) { |
|
||||||
const since = dateTimeToUnixTimestamp(filterSince); |
|
||||||
if (since !== null && since > 0) { |
|
||||||
filter.since = since; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterUntil.trim()) { |
|
||||||
const until = dateTimeToUnixTimestamp(filterUntil); |
|
||||||
if (until !== null && until > 0) { |
|
||||||
filter.until = until; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (filterLimit > 0) { |
|
||||||
filter.limit = filterLimit; |
|
||||||
} else { |
|
||||||
filter.limit = 100; |
|
||||||
} |
|
||||||
|
|
||||||
return filter; |
|
||||||
} |
|
||||||
|
|
||||||
async function handleSearch() { |
|
||||||
searching = true; |
|
||||||
const newEventRelayMap = new Map<string, string>(); |
|
||||||
|
|
||||||
try { |
|
||||||
await nostrClient.initialize(); |
|
||||||
const relays = relayManager.getAllAvailableRelays(); |
|
||||||
|
|
||||||
const filter = await buildFilter(); |
|
||||||
|
|
||||||
// Check if filter has any conditions (not just limit) |
|
||||||
const hasConditions = Object.keys(filter).some(key => key !== 'limit'); |
|
||||||
|
|
||||||
if (!hasConditions) { |
|
||||||
// No filter conditions, show error or return empty |
|
||||||
searchResults = { events: [], profiles: [], relays }; |
|
||||||
if (onSearchResults) { |
|
||||||
onSearchResults({ events: [], profiles: [], relays, eventRelays: new Map() }); |
|
||||||
} |
|
||||||
searching = false; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// Fetch events using the filter |
|
||||||
const events = await nostrClient.fetchEvents( |
|
||||||
[filter], |
|
||||||
relays, |
|
||||||
{ useCache: true, cacheResults: true } |
|
||||||
); |
|
||||||
|
|
||||||
// Track which relay each event came from |
|
||||||
for (const event of events) { |
|
||||||
if (!newEventRelayMap.has(event.id)) { |
|
||||||
newEventRelayMap.set(event.id, relays[0] || 'unknown'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Sort by created_at (newest first) |
|
||||||
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at); |
|
||||||
|
|
||||||
searchResults = { events: sortedEvents, profiles: [], relays }; |
|
||||||
if (onSearchResults) { |
|
||||||
onSearchResults({ events: sortedEvents, profiles: [], relays, eventRelays: newEventRelayMap }); |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
console.error('Advanced search error:', error); |
|
||||||
searchResults = { events: [], profiles: [], relays: [] }; |
|
||||||
if (onSearchResults) { |
|
||||||
onSearchResults({ events: [], profiles: [], relays: [], eventRelays: new Map() }); |
|
||||||
} |
|
||||||
} finally { |
|
||||||
searching = false; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export function clearSearch() { |
|
||||||
filterIds = ''; |
|
||||||
filterAuthors = ''; |
|
||||||
filterKinds = ''; |
|
||||||
filterE = ''; |
|
||||||
filterP = ''; |
|
||||||
filterA = ''; |
|
||||||
filterQ = ''; |
|
||||||
filterT = ''; |
|
||||||
filterC = ''; |
|
||||||
filterD = ''; |
|
||||||
filterSince = ''; |
|
||||||
filterUntil = ''; |
|
||||||
filterLimit = 100; |
|
||||||
searchResults = { events: [], profiles: [] }; |
|
||||||
eventRelayMap.clear(); |
|
||||||
if (onSearchResults) { |
|
||||||
onSearchResults({ events: [], profiles: [], relays: [], eventRelays: new Map() }); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export function getSearchResults() { |
|
||||||
return searchResults; |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="advanced-search"> |
|
||||||
<h2>Advanced Search (NIP-01 Filters)</h2> |
|
||||||
<p class="section-description"> |
|
||||||
Use NIP-01 standard filters to search for events. All filters are optional - combine them as needed. |
|
||||||
</p> |
|
||||||
|
|
||||||
<div class="search-container"> |
|
||||||
<div class="advanced-filters-grid"> |
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-ids" class="filter-label">IDs (comma-separated event IDs):</label> |
|
||||||
<input |
|
||||||
id="filter-ids" |
|
||||||
type="text" |
|
||||||
bind:value={filterIds} |
|
||||||
placeholder="64-char hex event IDs or bech32 (note, nevent)" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-authors" class="filter-label">Authors (comma-separated pubkeys):</label> |
|
||||||
<input |
|
||||||
id="filter-authors" |
|
||||||
type="text" |
|
||||||
bind:value={filterAuthors} |
|
||||||
placeholder="64-char hex pubkeys or bech32 (npub, nprofile)" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-kinds" class="filter-label">Kinds (comma-separated):</label> |
|
||||||
<input |
|
||||||
id="filter-kinds" |
|
||||||
type="text" |
|
||||||
bind:value={filterKinds} |
|
||||||
placeholder="e.g., 1, 7, 11" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-e" class="filter-label">#e tag (comma-separated event IDs):</label> |
|
||||||
<input |
|
||||||
id="filter-e" |
|
||||||
type="text" |
|
||||||
bind:value={filterE} |
|
||||||
placeholder="64-char hex event IDs or bech32" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-p" class="filter-label">#p tag (comma-separated pubkeys):</label> |
|
||||||
<input |
|
||||||
id="filter-p" |
|
||||||
type="text" |
|
||||||
bind:value={filterP} |
|
||||||
placeholder="64-char hex pubkeys or bech32" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-a" class="filter-label">#a tag (comma-separated addressable refs):</label> |
|
||||||
<input |
|
||||||
id="filter-a" |
|
||||||
type="text" |
|
||||||
bind:value={filterA} |
|
||||||
placeholder="kind:pubkey:d-tag" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-q" class="filter-label">#q tag (comma-separated quoted event IDs):</label> |
|
||||||
<input |
|
||||||
id="filter-q" |
|
||||||
type="text" |
|
||||||
bind:value={filterQ} |
|
||||||
placeholder="64-char hex event IDs or bech32" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-t" class="filter-label">#T tag (comma-separated topics):</label> |
|
||||||
<input |
|
||||||
id="filter-t" |
|
||||||
type="text" |
|
||||||
bind:value={filterT} |
|
||||||
placeholder="topic1, topic2, topic3" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-c" class="filter-label">#C tag (comma-separated categories):</label> |
|
||||||
<input |
|
||||||
id="filter-c" |
|
||||||
type="text" |
|
||||||
bind:value={filterC} |
|
||||||
placeholder="category1, category2, category3" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-d" class="filter-label">#d tag (comma-separated d-tag values):</label> |
|
||||||
<input |
|
||||||
id="filter-d" |
|
||||||
type="text" |
|
||||||
bind:value={filterD} |
|
||||||
placeholder="d-tag1, d-tag2, d-tag3" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-since" class="filter-label">Since (date-time or unix timestamp):</label> |
|
||||||
<input |
|
||||||
id="filter-since" |
|
||||||
type="text" |
|
||||||
bind:value={filterSince} |
|
||||||
placeholder="2024-01-15T14:30:00Z or unix timestamp" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
<small class="filter-hint">Accepts ISO 8601 format or unix timestamp</small> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-until" class="filter-label">Until (date-time or unix timestamp):</label> |
|
||||||
<input |
|
||||||
id="filter-until" |
|
||||||
type="text" |
|
||||||
bind:value={filterUntil} |
|
||||||
placeholder="2024-01-15T14:30:00Z or unix timestamp" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
<small class="filter-hint">Accepts ISO 8601 format or unix timestamp</small> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="filter-group"> |
|
||||||
<label for="filter-limit" class="filter-label">Limit:</label> |
|
||||||
<input |
|
||||||
id="filter-limit" |
|
||||||
type="number" |
|
||||||
bind:value={filterLimit} |
|
||||||
min="1" |
|
||||||
max="1000" |
|
||||||
class="filter-input" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="search-button-wrapper"> |
|
||||||
<button |
|
||||||
class="search-button" |
|
||||||
onclick={handleSearch} |
|
||||||
disabled={searching} |
|
||||||
aria-label="Search" |
|
||||||
> |
|
||||||
{searching ? 'Searching...' : 'Search'} |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<style> |
|
||||||
.advanced-search { |
|
||||||
/* No container styling - parent .find-section handles it */ |
|
||||||
} |
|
||||||
|
|
||||||
.advanced-search h2 { |
|
||||||
margin: 0 0 1rem 0; |
|
||||||
font-size: 1.25em; |
|
||||||
font-weight: 600; |
|
||||||
color: var(--fog-text, #475569); |
|
||||||
} |
|
||||||
|
|
||||||
@media (min-width: 640px) { |
|
||||||
.advanced-search h2 { |
|
||||||
margin: 0 0 1.5rem 0; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .advanced-search h2 { |
|
||||||
color: var(--fog-dark-text, #cbd5e1); |
|
||||||
} |
|
||||||
|
|
||||||
.section-description { |
|
||||||
margin: 0 0 1rem 0; |
|
||||||
color: var(--fog-text-light, #52667a); |
|
||||||
font-size: 0.875em; |
|
||||||
} |
|
||||||
|
|
||||||
@media (min-width: 640px) { |
|
||||||
.section-description { |
|
||||||
margin: 0 0 1.5rem 0; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .section-description { |
|
||||||
color: var(--fog-dark-text-light, #a8b8d0); |
|
||||||
} |
|
||||||
|
|
||||||
.search-container { |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
gap: 1.5rem; |
|
||||||
} |
|
||||||
|
|
||||||
.advanced-filters-grid { |
|
||||||
display: grid; |
|
||||||
grid-template-columns: 1fr; |
|
||||||
gap: 0.75rem; |
|
||||||
padding: 0.75rem; |
|
||||||
background: var(--fog-highlight, #f3f4f6); |
|
||||||
border-radius: 0.375rem; |
|
||||||
border: 1px solid var(--fog-border, #e5e7eb); |
|
||||||
} |
|
||||||
|
|
||||||
@media (min-width: 640px) { |
|
||||||
.advanced-filters-grid { |
|
||||||
gap: 1rem; |
|
||||||
padding: 1rem; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .advanced-filters-grid { |
|
||||||
background: var(--fog-dark-highlight, #374151); |
|
||||||
border-color: var(--fog-dark-border, #475569); |
|
||||||
} |
|
||||||
|
|
||||||
@media (min-width: 768px) { |
|
||||||
.advanced-filters-grid { |
|
||||||
grid-template-columns: repeat(2, 1fr); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.filter-group { |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
gap: 0.5rem; |
|
||||||
} |
|
||||||
|
|
||||||
.filter-label { |
|
||||||
font-size: 0.875em; |
|
||||||
font-weight: 500; |
|
||||||
color: var(--fog-text, #1f2937); |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .filter-label { |
|
||||||
color: var(--fog-dark-text, #f9fafb); |
|
||||||
} |
|
||||||
|
|
||||||
.filter-input { |
|
||||||
padding: 0.5em; |
|
||||||
border: 1px solid var(--fog-border, #cbd5e1); |
|
||||||
border-radius: 0.375rem; |
|
||||||
background: var(--fog-post, #ffffff); |
|
||||||
color: var(--fog-text, #1f2937); |
|
||||||
font-size: 0.875em; |
|
||||||
font-family: inherit; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
.filter-input:focus { |
|
||||||
outline: none; |
|
||||||
border-color: var(--fog-accent, #64748b); |
|
||||||
box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1); |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .filter-input { |
|
||||||
border-color: var(--fog-dark-border, #374151); |
|
||||||
background: var(--fog-dark-post, #1f2937); |
|
||||||
color: var(--fog-dark-text, #f9fafb); |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .filter-input:focus { |
|
||||||
border-color: var(--fog-dark-accent, #94a3b8); |
|
||||||
box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.1); |
|
||||||
} |
|
||||||
|
|
||||||
.filter-hint { |
|
||||||
font-size: 0.75em; |
|
||||||
color: var(--fog-text-light, #52667a); |
|
||||||
margin-top: 0.25em; |
|
||||||
display: block; |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .filter-hint { |
|
||||||
color: var(--fog-dark-text-light, #a8b8d0); |
|
||||||
} |
|
||||||
|
|
||||||
.search-button-wrapper { |
|
||||||
display: flex; |
|
||||||
justify-content: flex-end; |
|
||||||
} |
|
||||||
|
|
||||||
.search-button { |
|
||||||
padding: 0.75em 1.5em; |
|
||||||
background: var(--fog-accent, #64748b); |
|
||||||
color: #ffffff; |
|
||||||
border: none; |
|
||||||
border-radius: 0.375rem; |
|
||||||
cursor: pointer; |
|
||||||
font-size: 0.875em; |
|
||||||
font-weight: 500; |
|
||||||
font-family: inherit; |
|
||||||
white-space: nowrap; |
|
||||||
transition: all 0.2s; |
|
||||||
min-width: 100px; |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .search-button { |
|
||||||
background: var(--fog-dark-accent, #94a3b8); |
|
||||||
color: #1f2937; |
|
||||||
} |
|
||||||
|
|
||||||
.search-button:hover:not(:disabled) { |
|
||||||
opacity: 0.9; |
|
||||||
transform: translateY(-1px); |
|
||||||
} |
|
||||||
|
|
||||||
.search-button:active:not(:disabled) { |
|
||||||
transform: translateY(0); |
|
||||||
} |
|
||||||
|
|
||||||
.search-button:disabled { |
|
||||||
opacity: 0.6; |
|
||||||
cursor: not-allowed; |
|
||||||
} |
|
||||||
</style> |
|
||||||
Loading…
Reference in new issue