Edit from phone contacts
This commit is contained in:
@@ -7,9 +7,11 @@ import { toFaDigits } from '@/lib/jalali';
|
|||||||
import { UiButton, UiModal } from '@/components/ui';
|
import { UiButton, UiModal } from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import {
|
import {
|
||||||
|
filterPhoneContacts,
|
||||||
isContactPickerSupported,
|
isContactPickerSupported,
|
||||||
parseVCardFile,
|
parseVCardFile,
|
||||||
pickPhoneContacts,
|
pickPhoneContacts,
|
||||||
|
sortPhoneContacts,
|
||||||
type PhoneContactDraft,
|
type PhoneContactDraft,
|
||||||
} from '@/lib/phone-contacts';
|
} from '@/lib/phone-contacts';
|
||||||
|
|
||||||
@@ -30,18 +32,27 @@ const phase = ref<'closed' | 'help' | 'review'>('closed');
|
|||||||
const importing = ref(false);
|
const importing = ref(false);
|
||||||
const drafts = ref<PhoneContactDraft[]>([]);
|
const drafts = ref<PhoneContactDraft[]>([]);
|
||||||
const selected = ref<Set<string>>(new Set());
|
const selected = ref<Set<string>>(new Set());
|
||||||
|
const listQuery = ref('');
|
||||||
const fileInput = ref<HTMLInputElement | null>(null);
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const modalOpen = computed(() => phase.value !== 'closed');
|
const modalOpen = computed(() => phase.value !== 'closed');
|
||||||
const modalTitle = computed(() =>
|
const modalTitle = computed(() =>
|
||||||
phase.value === 'review' ? 'انتخاب مخاطبین برای ورود' : 'ورود از مخاطبین گوشی',
|
phase.value === 'review' ? 'انتخاب مخاطبین برای ورود' : 'ورود از مخاطبین گوشی',
|
||||||
);
|
);
|
||||||
|
const visibleDrafts = computed(() =>
|
||||||
|
filterPhoneContacts(drafts.value, listQuery.value),
|
||||||
|
);
|
||||||
const selectedCount = computed(
|
const selectedCount = computed(
|
||||||
() => drafts.value.filter((d) => selected.value.has(d.key)).length,
|
() => drafts.value.filter((d) => selected.value.has(d.key)).length,
|
||||||
);
|
);
|
||||||
const allSelected = computed(
|
const allSelected = computed(
|
||||||
() => drafts.value.length > 0 && selectedCount.value === drafts.value.length,
|
() => drafts.value.length > 0 && selectedCount.value === drafts.value.length,
|
||||||
);
|
);
|
||||||
|
const visibleAllSelected = computed(
|
||||||
|
() =>
|
||||||
|
visibleDrafts.value.length > 0 &&
|
||||||
|
visibleDrafts.value.every((d) => selected.value.has(d.key)),
|
||||||
|
);
|
||||||
|
|
||||||
function apiMessage(e: unknown, fallback: string) {
|
function apiMessage(e: unknown, fallback: string) {
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
@@ -52,8 +63,9 @@ function apiMessage(e: unknown, fallback: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function showReview(rows: PhoneContactDraft[], opts?: { selectAll?: boolean }) {
|
async function showReview(rows: PhoneContactDraft[], opts?: { selectAll?: boolean }) {
|
||||||
drafts.value = rows;
|
drafts.value = sortPhoneContacts(rows);
|
||||||
selected.value = opts?.selectAll ? new Set(rows.map((r) => r.key)) : new Set();
|
listQuery.value = '';
|
||||||
|
selected.value = opts?.selectAll ? new Set(drafts.value.map((r) => r.key)) : new Set();
|
||||||
// بعد از دیالوگ فایل، یک تیک صبر کن تا مودال مطمئن باز شود
|
// بعد از دیالوگ فایل، یک تیک صبر کن تا مودال مطمئن باز شود
|
||||||
phase.value = 'closed';
|
phase.value = 'closed';
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -68,13 +80,25 @@ function toggle(key: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectAll(on: boolean) {
|
function selectAll(on: boolean) {
|
||||||
selected.value = on ? new Set(drafts.value.map((d) => d.key)) : new Set();
|
if (!on) {
|
||||||
|
selected.value = new Set();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// اگر جستجو فعال است فقط نتایج را انتخاب کن، وگرنه همه
|
||||||
|
if (listQuery.value.trim()) {
|
||||||
|
const next = new Set(selected.value);
|
||||||
|
for (const row of visibleDrafts.value) next.add(row.key);
|
||||||
|
selected.value = next;
|
||||||
|
} else {
|
||||||
|
selected.value = new Set(drafts.value.map((d) => d.key));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAll() {
|
function closeAll() {
|
||||||
phase.value = 'closed';
|
phase.value = 'closed';
|
||||||
drafts.value = [];
|
drafts.value = [];
|
||||||
selected.value = new Set();
|
selected.value = new Set();
|
||||||
|
listQuery.value = '';
|
||||||
importing.value = false;
|
importing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +122,6 @@ async function startPicker() {
|
|||||||
emit('fail', 'مخاطبی انتخاب نشد.');
|
emit('fail', 'مخاطبی انتخاب نشد.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// از Contact Picker قبلاً انتخاب شدهاند → همه تیکخورده، ولی هنوز قابل کم/زیاد کردن
|
|
||||||
await showReview(rows, { selectAll: true });
|
await showReview(rows, { selectAll: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const name = e instanceof DOMException ? e.name : '';
|
const name = e instanceof DOMException ? e.name : '';
|
||||||
@@ -124,7 +147,6 @@ async function onVcfFile(ev: Event) {
|
|||||||
phase.value = 'help';
|
phase.value = 'help';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// از فایل: هیچکدام پیشفرض انتخاب نشود تا کاربر خودش یک/چند/همه را بزند
|
|
||||||
await showReview(rows, { selectAll: false });
|
await showReview(rows, { selectAll: false });
|
||||||
} catch {
|
} catch {
|
||||||
emit('fail', 'خواندن فایل مخاطبین (.vcf) ناموفق بود.');
|
emit('fail', 'خواندن فایل مخاطبین (.vcf) ناموفق بود.');
|
||||||
@@ -205,16 +227,35 @@ async function importSelected() {
|
|||||||
|
|
||||||
<template v-else-if="phase === 'review'">
|
<template v-else-if="phase === 'review'">
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
{{ toFaDigits(drafts.length) }} مخاطب از فایل/گوشی خوانده شد.
|
{{ toFaDigits(drafts.length) }} مخاطب از فایل/گوشی خوانده شد (مرتبشده الفبایی).
|
||||||
موارد دلخواه را تیک بزنید؛ فقط انتخابشدهها وارد میشوند.
|
موارد دلخواه را تیک بزنید؛ فقط انتخابشدهها وارد میشوند.
|
||||||
</p>
|
</p>
|
||||||
<div class="toolbar">
|
<div class="search">
|
||||||
<button type="button" class="link" @click="selectAll(true)">انتخاب همه</button>
|
<UiIcon name="search" :size="15" />
|
||||||
<button type="button" class="link" @click="selectAll(false)">هیچکدام</button>
|
<input
|
||||||
<span class="count">{{ toFaDigits(selectedCount) }} از {{ toFaDigits(drafts.length) }}</span>
|
v-model="listQuery"
|
||||||
|
type="search"
|
||||||
|
placeholder="جستجو نام یا موبایل…"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list">
|
<div class="toolbar">
|
||||||
<li v-for="row in drafts" :key="row.key">
|
<button type="button" class="link" @click="selectAll(true)">
|
||||||
|
{{ listQuery.trim() ? 'انتخاب نتایج' : 'انتخاب همه' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="link" @click="selectAll(false)">هیچکدام</button>
|
||||||
|
<span class="count">
|
||||||
|
{{ toFaDigits(selectedCount) }} انتخاب
|
||||||
|
<template v-if="listQuery.trim()">
|
||||||
|
· {{ toFaDigits(visibleDrafts.length) }} نتیجه
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
از {{ toFaDigits(drafts.length) }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul v-if="visibleDrafts.length" class="list">
|
||||||
|
<li v-for="row in visibleDrafts" :key="row.key">
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -230,6 +271,7 @@ async function importSelected() {
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p v-else class="empty">مخاطبی با این جستجو پیدا نشد.</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -251,9 +293,12 @@ async function importSelected() {
|
|||||||
@click="importSelected"
|
@click="importSelected"
|
||||||
>
|
>
|
||||||
<UiIcon name="user-plus" :size="16" />
|
<UiIcon name="user-plus" :size="16" />
|
||||||
<template v-if="allSelected && selectedCount">
|
<template v-if="allSelected && selectedCount && !listQuery.trim()">
|
||||||
ورود همه ({{ toFaDigits(selectedCount) }})
|
ورود همه ({{ toFaDigits(selectedCount) }})
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="visibleAllSelected && listQuery.trim() && selectedCount">
|
||||||
|
ورود {{ toFaDigits(selectedCount) }} نفر
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
ورود {{ toFaDigits(selectedCount) }} نفر
|
ورود {{ toFaDigits(selectedCount) }} نفر
|
||||||
</template>
|
</template>
|
||||||
@@ -292,6 +337,28 @@ async function importSelected() {
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.65rem;
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -367,4 +434,11 @@ async function importSelected() {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -232,3 +232,36 @@ export async function parseVCardFile(file: File): Promise<PhoneContactDraft[]> {
|
|||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
return parseVCard(text);
|
return parseVCard(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toEnDigits(raw: string): string {
|
||||||
|
return raw.replace(/[۰-۹٠-٩]/g, (ch) => {
|
||||||
|
const fa = '۰۱۲۳۴۵۶۷۸۹'.indexOf(ch);
|
||||||
|
if (fa >= 0) return String(fa);
|
||||||
|
const ar = '٠١٢٣٤٥٦٧٨٩'.indexOf(ch);
|
||||||
|
return ar >= 0 ? String(ar) : ch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortPhoneContacts(rows: PhoneContactDraft[]): PhoneContactDraft[] {
|
||||||
|
return [...rows].sort((a, b) =>
|
||||||
|
a.fullName.localeCompare(b.fullName, 'fa', { sensitivity: 'base', numeric: true }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterPhoneContacts(
|
||||||
|
rows: PhoneContactDraft[],
|
||||||
|
query: string,
|
||||||
|
): PhoneContactDraft[] {
|
||||||
|
const needle = toEnDigits(query).trim().toLowerCase();
|
||||||
|
if (!needle) return rows;
|
||||||
|
const digitNeedle = needle.replace(/\D/g, '');
|
||||||
|
return rows.filter((row) => {
|
||||||
|
const name = toEnDigits(row.fullName).toLowerCase();
|
||||||
|
if (name.includes(needle)) return true;
|
||||||
|
const phones = [row.mobile, row.phone].filter(Boolean).join(' ');
|
||||||
|
const phonesNorm = toEnDigits(phones).toLowerCase();
|
||||||
|
if (phonesNorm.includes(needle)) return true;
|
||||||
|
if (digitNeedle && phonesNorm.replace(/\D/g, '').includes(digitNeedle)) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { CompanyDto, CreatePersonRequest, PersonDto, PersonMood } from '@mento/shared';
|
import type { CompanyDto, CreatePersonRequest, PersonDto, PersonMood } from '@mento/shared';
|
||||||
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
|
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
|
||||||
|
import { toFaDigits } from '@/lib/jalali';
|
||||||
import {
|
import {
|
||||||
|
filterPhoneContacts,
|
||||||
isContactPickerSupported,
|
isContactPickerSupported,
|
||||||
parseVCardFile,
|
parseVCardFile,
|
||||||
pickOnePhoneContact,
|
pickOnePhoneContact,
|
||||||
|
sortPhoneContacts,
|
||||||
type PhoneContactDraft,
|
type PhoneContactDraft,
|
||||||
} from '@/lib/phone-contacts';
|
} from '@/lib/phone-contacts';
|
||||||
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
|
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
|
||||||
@@ -21,6 +24,7 @@ import {
|
|||||||
UiCard,
|
UiCard,
|
||||||
UiField,
|
UiField,
|
||||||
UiInput,
|
UiInput,
|
||||||
|
UiModal,
|
||||||
UiPageHeader,
|
UiPageHeader,
|
||||||
UiSelect,
|
UiSelect,
|
||||||
UiTextarea,
|
UiTextarea,
|
||||||
@@ -65,6 +69,13 @@ const loyaltyTierName = ref('');
|
|||||||
const contactPickerSupported = isContactPickerSupported();
|
const contactPickerSupported = isContactPickerSupported();
|
||||||
const pickingContact = ref(false);
|
const pickingContact = ref(false);
|
||||||
const contactFileInput = ref<HTMLInputElement | null>(null);
|
const contactFileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const pickOpen = ref(false);
|
||||||
|
const pickDrafts = ref<PhoneContactDraft[]>([]);
|
||||||
|
const pickQuery = ref('');
|
||||||
|
|
||||||
|
const visiblePickDrafts = computed(() =>
|
||||||
|
filterPhoneContacts(pickDrafts.value, pickQuery.value),
|
||||||
|
);
|
||||||
|
|
||||||
const backTo = computed(() =>
|
const backTo = computed(() =>
|
||||||
isNew.value ? { name: 'contacts' as const } : { name: 'person-detail' as const, params: { id: id.value } },
|
isNew.value ? { name: 'contacts' as const } : { name: 'person-detail' as const, params: { id: id.value } },
|
||||||
@@ -91,6 +102,29 @@ function applyContactDraft(draft: PhoneContactDraft) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function draftLine(row: PhoneContactDraft) {
|
||||||
|
return [row.mobile, row.phone, row.email].filter(Boolean).join(' · ') || 'بدون شماره/ایمیل';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePick() {
|
||||||
|
pickOpen.value = false;
|
||||||
|
pickDrafts.value = [];
|
||||||
|
pickQuery.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function choosePick(row: PhoneContactDraft) {
|
||||||
|
applyContactDraft(row);
|
||||||
|
closePick();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPickList(rows: PhoneContactDraft[]) {
|
||||||
|
pickDrafts.value = sortPhoneContacts(rows);
|
||||||
|
pickQuery.value = '';
|
||||||
|
pickOpen.value = false;
|
||||||
|
await nextTick();
|
||||||
|
pickOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
async function fillFromPhoneContact() {
|
async function fillFromPhoneContact() {
|
||||||
if (pickingContact.value || saving.value) return;
|
if (pickingContact.value || saving.value) return;
|
||||||
pickingContact.value = true;
|
pickingContact.value = true;
|
||||||
@@ -126,12 +160,11 @@ async function onContactVcfFile(ev: Event) {
|
|||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
const rows = await parseVCardFile(file);
|
const rows = await parseVCardFile(file);
|
||||||
const draft = rows[0];
|
if (!rows.length) {
|
||||||
if (!draft) {
|
|
||||||
error.value = 'مخاطبی در فایل پیدا نشد.';
|
error.value = 'مخاطبی در فایل پیدا نشد.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyContactDraft(draft);
|
await openPickList(rows);
|
||||||
} catch {
|
} catch {
|
||||||
error.value = 'خواندن فایل مخاطب (.vcf) ناموفق بود.';
|
error.value = 'خواندن فایل مخاطب (.vcf) ناموفق بود.';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -306,6 +339,38 @@ onMounted(load);
|
|||||||
@change="onContactVcfFile"
|
@change="onContactVcfFile"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UiModal :open="pickOpen" title="انتخاب مخاطب" @close="closePick">
|
||||||
|
<p class="pick-lead">
|
||||||
|
{{ toFaDigits(pickDrafts.length) }} مخاطب در فایل (مرتبشده الفبایی).
|
||||||
|
یکی را انتخاب کنید تا فقط موبایل (و در صورت خالی بودن، نام) پر شود.
|
||||||
|
</p>
|
||||||
|
<div class="pick-search">
|
||||||
|
<UiIcon name="search" :size="15" />
|
||||||
|
<input
|
||||||
|
v-model="pickQuery"
|
||||||
|
type="search"
|
||||||
|
placeholder="جستجو نام یا موبایل…"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul v-if="visiblePickDrafts.length" class="pick-list">
|
||||||
|
<li v-for="row in visiblePickDrafts" :key="row.key">
|
||||||
|
<button type="button" class="pick-row" @click="choosePick(row)">
|
||||||
|
<span class="pick-avatar" aria-hidden="true">{{ row.fullName.slice(0, 1) }}</span>
|
||||||
|
<span class="pick-meta">
|
||||||
|
<strong>{{ row.fullName }}</strong>
|
||||||
|
<small>{{ draftLine(row) }}</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p v-else class="pick-empty">مخاطبی با این جستجو پیدا نشد.</p>
|
||||||
|
<template #footer>
|
||||||
|
<UiButton variant="ghost" @click="closePick">انصراف</UiButton>
|
||||||
|
<UiButton variant="ghost" @click="contactFileInput?.click()">فایل دیگر</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiModal>
|
||||||
|
|
||||||
<UiField label="تلفن" for-id="phone">
|
<UiField label="تلفن" for-id="phone">
|
||||||
<UiInput id="phone" v-model="phone" type="tel" />
|
<UiInput id="phone" v-model="phone" type="tel" />
|
||||||
</UiField>
|
</UiField>
|
||||||
@@ -489,6 +554,102 @@ onMounted(load);
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pick-lead {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.65rem;
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-search input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
max-height: min(48vh, 360px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-align: start;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-row:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-avatar {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--accent-subtle);
|
||||||
|
color: var(--accent-deep);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.1rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-meta strong {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-meta small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-empty {
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 901px) {
|
@media (min-width: 901px) {
|
||||||
.mob-only {
|
.mob-only {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user