|
| 1 | +<script setup lang="ts"> |
| 2 | + import { Input, Switch, Toggle } from '@vuetify/v0' |
| 3 | + import { computed, ref, shallowRef } from 'vue' |
| 4 | + import MessageRow from './MessageRow.vue' |
| 5 | + import { messages } from './messages' |
| 6 | +
|
| 7 | + const query = shallowRef('search') |
| 8 | + const serverMode = shallowRef(false) |
| 9 | +
|
| 10 | + const filters = ['budget', 'review', 'urgent'] as const |
| 11 | + const active = ref<Record<string, boolean>>({}) |
| 12 | +
|
| 13 | + const terms = computed(() => { |
| 14 | + const out: string[] = [] |
| 15 | + if (query.value.trim()) out.push(query.value.trim()) |
| 16 | + for (const f of filters) { |
| 17 | + if (active.value[f] && !out.some(t => t.toLowerCase() === f)) out.push(f) |
| 18 | + } |
| 19 | + return out |
| 20 | + }) |
| 21 | +
|
| 22 | + const matching = computed(() => { |
| 23 | + if (terms.value.length === 0) return messages |
| 24 | + return messages.filter(m => { |
| 25 | + const haystack = (m.subject + ' ' + m.body).toLowerCase() |
| 26 | + return terms.value.some(t => haystack.includes(t.toLowerCase())) |
| 27 | + }) |
| 28 | + }) |
| 29 | +</script> |
| 30 | + |
| 31 | +<template> |
| 32 | + <div class="flex flex-col gap-3 p-4 max-w-2xl mx-auto"> |
| 33 | + <Input.Root id="inbox-search" v-model="query" label="Search inbox"> |
| 34 | + <Input.Control |
| 35 | + class="w-full px-3 py-2 rounded-lg border border-divider bg-surface text-on-surface placeholder:text-on-surface/40 outline-none data-[focused]:border-primary transition-colors" |
| 36 | + placeholder="Search by sender, subject, or body…" |
| 37 | + /> |
| 38 | + </Input.Root> |
| 39 | + |
| 40 | + <div class="flex flex-wrap items-center gap-2"> |
| 41 | + <span class="text-xs font-medium text-on-surface/60">Saved filters:</span> |
| 42 | + |
| 43 | + <Toggle.Root |
| 44 | + v-for="f in filters" |
| 45 | + :key="f" |
| 46 | + v-model="active[f]" |
| 47 | + class="px-2.5 py-1 rounded-full border border-divider text-xs font-medium text-on-surface/70 transition-colors data-[state=on]:border-primary data-[state=on]:bg-primary/15 data-[state=on]:text-on-surface hover:bg-surface-tint" |
| 48 | + > |
| 49 | + {{ f }} |
| 50 | + </Toggle.Root> |
| 51 | + |
| 52 | + <label class="ml-auto inline-flex items-center gap-2 text-xs text-on-surface/70 cursor-pointer"> |
| 53 | + <Switch.Root |
| 54 | + v-model="serverMode" |
| 55 | + class="inline-flex items-center border-none bg-transparent p-0 outline-none" |
| 56 | + > |
| 57 | + <Switch.Track class="relative inline-flex items-center w-9 h-5 rounded-full bg-surface-variant transition-colors data-[state=checked]:bg-success"> |
| 58 | + <Switch.Thumb class="![visibility:visible] block size-3.5 rounded-full bg-white shadow-sm transition-transform translate-x-0.5 data-[state=checked]:translate-x-5" /> |
| 59 | + </Switch.Track> |
| 60 | + </Switch.Root> |
| 61 | + Server snippets |
| 62 | + </label> |
| 63 | + </div> |
| 64 | + |
| 65 | + <div class="flex items-baseline justify-between text-xs text-on-surface/60 px-1"> |
| 66 | + <span>{{ matching.length }} of {{ messages.length }} threads</span> |
| 67 | + |
| 68 | + <span v-if="terms.length > 0"> |
| 69 | + Highlighting |
| 70 | + <span class="font-mono">{{ JSON.stringify(terms.length === 1 ? terms[0] : terms) }}</span> |
| 71 | + </span> |
| 72 | + </div> |
| 73 | + |
| 74 | + <div class="flex flex-col rounded-lg border border-divider bg-surface divide-y divide-divider"> |
| 75 | + <MessageRow |
| 76 | + v-for="message in matching" |
| 77 | + :key="message.id" |
| 78 | + :message |
| 79 | + :server-mode |
| 80 | + :terms |
| 81 | + /> |
| 82 | + |
| 83 | + <p v-if="matching.length === 0" class="p-6 text-center text-sm text-on-surface/60"> |
| 84 | + No threads match the active filters. |
| 85 | + </p> |
| 86 | + </div> |
| 87 | + </div> |
| 88 | +</template> |
0 commit comments