2026-09-02 21:29:00 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
|
|
|
|
import { api } from '@/lib/api';
|
|
|
|
|
|
import {
|
|
|
|
|
|
calendarDaysBetween,
|
|
|
|
|
|
formatClockFa,
|
|
|
|
|
|
formatJalaliDay,
|
|
|
|
|
|
} from '@/lib/jalali';
|
2026-11-02 16:34:03 +03:30
|
|
|
|
import { prepareActivityAttachment } from '@/lib/activity-attachment';
|
|
|
|
|
|
import { startVoiceRecorder, type VoiceRecorder } from '@/lib/voice-note';
|
2026-09-03 14:20:55 +03:30
|
|
|
|
import { appendSpokenText } from '@/lib/spoken-text';
|
2026-09-02 21:29:00 +03:30
|
|
|
|
import UiIcon from '@/components/UiIcon.vue';
|
2026-09-03 14:20:55 +03:30
|
|
|
|
import VoiceToTextButton from '@/components/VoiceToTextButton.vue';
|
2026-09-02 21:29:00 +03:30
|
|
|
|
import type { ActivityDto, ActivityType, OrganizationDto, OrgUserDto } from '@mento/shared';
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
activity: ActivityDto;
|
|
|
|
|
|
saving?: boolean;
|
2026-10-27 21:52:37 +03:30
|
|
|
|
save: (payload: { text: string; photoOnly: boolean }) => Promise<void>;
|
2026-09-02 21:29:00 +03:30
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
close: [];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const typeIcon: Record<ActivityType, 'phone' | 'users' | 'check' | 'chat'> = {
|
|
|
|
|
|
call: 'phone',
|
|
|
|
|
|
meeting: 'users',
|
|
|
|
|
|
task: 'check',
|
|
|
|
|
|
note: 'chat',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-10-27 21:52:37 +03:30
|
|
|
|
const PHOTO_LIMIT = 8;
|
2026-11-02 16:34:03 +03:30
|
|
|
|
const VOICE_LIMIT = 5;
|
|
|
|
|
|
const FILE_LIMIT = 8;
|
2026-10-27 21:52:37 +03:30
|
|
|
|
type PendingPhoto = { localId: string; name: string; dataUrl: string; file: File };
|
2026-11-02 16:34:03 +03:30
|
|
|
|
type PendingVoice = { localId: string; name: string; url: string; file: File };
|
|
|
|
|
|
type PendingFile = { localId: string; name: string; file: File };
|
2026-10-27 21:52:37 +03:30
|
|
|
|
|
2026-09-02 21:29:00 +03:30
|
|
|
|
const comment = ref('');
|
|
|
|
|
|
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
2026-10-27 21:52:37 +03:30
|
|
|
|
const pendingPhotos = ref<PendingPhoto[]>([]);
|
2026-11-02 16:34:03 +03:30
|
|
|
|
const pendingVoices = ref<PendingVoice[]>([]);
|
|
|
|
|
|
const pendingFiles = ref<PendingFile[]>([]);
|
2026-10-27 21:52:37 +03:30
|
|
|
|
const uploadedCount = ref(0);
|
|
|
|
|
|
const photoBusy = ref(false);
|
|
|
|
|
|
const uploading = ref(false);
|
|
|
|
|
|
const photoError = ref('');
|
2026-11-02 16:34:03 +03:30
|
|
|
|
const fileInput = ref<HTMLInputElement | null>(null);
|
|
|
|
|
|
const voiceNoteOn = ref(false);
|
|
|
|
|
|
const voiceNoteSeconds = ref(0);
|
|
|
|
|
|
let voiceNote: VoiceRecorder | null = null;
|
2026-09-02 21:29:00 +03:30
|
|
|
|
const orgName = ref('سازمان');
|
|
|
|
|
|
const users = ref<OrgUserDto[]>([]);
|
|
|
|
|
|
const mentionOpen = ref(false);
|
|
|
|
|
|
const voiceTip = ref(localStorage.getItem('mento.voiceTipSeen') !== '1');
|
|
|
|
|
|
const voiceError = ref('');
|
|
|
|
|
|
const recording = ref(false);
|
2026-09-03 14:20:55 +03:30
|
|
|
|
const voiceBusy = ref(false);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
|
|
|
|
|
|
const subtitle = computed(() => {
|
|
|
|
|
|
const a = props.activity;
|
|
|
|
|
|
return a.deal?.title || a.company?.name || a.person?.fullName || '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const dueLabel = computed(() => {
|
|
|
|
|
|
const due = props.activity.dueAt;
|
|
|
|
|
|
if (!due) return 'بدون موعد';
|
|
|
|
|
|
const d = new Date(due);
|
|
|
|
|
|
const days = calendarDaysBetween(d, new Date());
|
|
|
|
|
|
const clock = formatClockFa(d);
|
|
|
|
|
|
if (days === 0) return `امروز ${clock}`;
|
|
|
|
|
|
if (days === 1) return `دیروز ${clock}`;
|
|
|
|
|
|
if (days === -1) return `فردا ${clock}`;
|
|
|
|
|
|
return `${formatJalaliDay(d)} ${clock}`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-11 16:48:37 +03:30
|
|
|
|
const now = ref(new Date());
|
|
|
|
|
|
let clockTimer: ReturnType<typeof setInterval> | undefined;
|
|
|
|
|
|
const nowLabel = computed(
|
|
|
|
|
|
() => `${formatJalaliDay(now.value)} ساعت ${formatClockFa(now.value)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-11-02 16:34:03 +03:30
|
|
|
|
function imageCount(kind: 'image' | 'audio' | 'file') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
props.activity.images?.filter((image) => {
|
|
|
|
|
|
const mime = image.mimeType.toLowerCase();
|
|
|
|
|
|
if (kind === 'audio') return mime.startsWith('audio/');
|
|
|
|
|
|
if (kind === 'image') return mime.startsWith('image/');
|
|
|
|
|
|
return !mime.startsWith('audio/') && !mime.startsWith('image/');
|
|
|
|
|
|
}).length ?? 0
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-10-27 21:52:37 +03:30
|
|
|
|
|
2026-09-03 14:20:55 +03:30
|
|
|
|
const canSubmit = computed(
|
2026-10-27 21:52:37 +03:30
|
|
|
|
() =>
|
2026-11-02 16:34:03 +03:30
|
|
|
|
(comment.value.trim().length > 0 ||
|
|
|
|
|
|
pendingPhotos.value.length > 0 ||
|
|
|
|
|
|
pendingVoices.value.length > 0 ||
|
|
|
|
|
|
pendingFiles.value.length > 0 ||
|
|
|
|
|
|
uploadedCount.value > 0) &&
|
2026-10-27 21:52:37 +03:30
|
|
|
|
!props.saving &&
|
|
|
|
|
|
!voiceBusy.value &&
|
|
|
|
|
|
!photoBusy.value &&
|
2026-11-02 16:34:03 +03:30
|
|
|
|
!uploading.value &&
|
|
|
|
|
|
!voiceNoteOn.value,
|
2026-09-03 14:20:55 +03:30
|
|
|
|
);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
|
2026-09-03 14:20:55 +03:30
|
|
|
|
function onVoiceTranscript(text: string) {
|
|
|
|
|
|
comment.value = appendSpokenText(comment.value, text);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 14:20:55 +03:30
|
|
|
|
function onVoiceBusy(active: boolean) {
|
|
|
|
|
|
voiceBusy.value = active;
|
|
|
|
|
|
if (active && voiceTip.value) {
|
2026-09-02 21:29:00 +03:30
|
|
|
|
voiceTip.value = false;
|
|
|
|
|
|
localStorage.setItem('mento.voiceTipSeen', '1');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 21:52:37 +03:30
|
|
|
|
function apiMessage(error: unknown, fallback: string) {
|
|
|
|
|
|
const raw = (error as { response?: { data?: { message?: string | string[] } } }).response?.data
|
|
|
|
|
|
?.message;
|
|
|
|
|
|
if (Array.isArray(raw)) return raw.join('، ');
|
|
|
|
|
|
if (typeof raw === 'string' && raw.trim()) return raw;
|
|
|
|
|
|
return fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-02 16:34:03 +03:30
|
|
|
|
async function onFilesPicked(event: Event) {
|
2026-10-27 21:52:37 +03:30
|
|
|
|
const input = event.target as HTMLInputElement;
|
|
|
|
|
|
const files = Array.from(input.files ?? []);
|
|
|
|
|
|
input.value = '';
|
|
|
|
|
|
if (!files.length) return;
|
|
|
|
|
|
photoError.value = '';
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
|
photoBusy.value = true;
|
|
|
|
|
|
try {
|
2026-11-02 16:34:03 +03:30
|
|
|
|
const prepared = await prepareActivityAttachment(file);
|
|
|
|
|
|
if (prepared.kind === 'image') {
|
|
|
|
|
|
if (imageCount('image') + pendingPhotos.value.length >= PHOTO_LIMIT) {
|
|
|
|
|
|
photoError.value = `حداکثر ${PHOTO_LIMIT} عکس برای هر فعالیت.`;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
pendingPhotos.value = [
|
|
|
|
|
|
...pendingPhotos.value,
|
|
|
|
|
|
{ localId: crypto.randomUUID(), name: prepared.name, dataUrl: prepared.previewUrl, file: prepared.file },
|
|
|
|
|
|
];
|
|
|
|
|
|
} else if (prepared.kind === 'audio') {
|
|
|
|
|
|
if (imageCount('audio') + pendingVoices.value.length >= VOICE_LIMIT) {
|
|
|
|
|
|
URL.revokeObjectURL(prepared.previewUrl);
|
|
|
|
|
|
photoError.value = `حداکثر ${VOICE_LIMIT} صوت برای هر فعالیت.`;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
pendingVoices.value = [
|
|
|
|
|
|
...pendingVoices.value,
|
|
|
|
|
|
{ localId: crypto.randomUUID(), name: prepared.name, url: prepared.previewUrl, file: prepared.file },
|
|
|
|
|
|
];
|
|
|
|
|
|
} else if (imageCount('file') + pendingFiles.value.length >= FILE_LIMIT) {
|
|
|
|
|
|
photoError.value = `حداکثر ${FILE_LIMIT} فایل برای هر فعالیت.`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pendingFiles.value = [
|
|
|
|
|
|
...pendingFiles.value,
|
|
|
|
|
|
{ localId: crypto.randomUUID(), name: prepared.name, file: prepared.file },
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
2026-10-27 21:52:37 +03:30
|
|
|
|
} catch (error) {
|
2026-11-02 16:34:03 +03:30
|
|
|
|
photoError.value = error instanceof Error ? error.message : 'افزودن فایل ناموفق بود.';
|
2026-10-27 21:52:37 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
photoBusy.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removePendingPhoto(localId: string) {
|
|
|
|
|
|
pendingPhotos.value = pendingPhotos.value.filter((photo) => photo.localId !== localId);
|
2026-11-02 16:34:03 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removePendingVoice(localId: string) {
|
|
|
|
|
|
const voice = pendingVoices.value.find((item) => item.localId === localId);
|
|
|
|
|
|
if (voice) URL.revokeObjectURL(voice.url);
|
|
|
|
|
|
pendingVoices.value = pendingVoices.value.filter((item) => item.localId !== localId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removePendingFile(localId: string) {
|
|
|
|
|
|
pendingFiles.value = pendingFiles.value.filter((item) => item.localId !== localId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function voiceClock(total: number) {
|
|
|
|
|
|
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addVoiceNote(file: File) {
|
|
|
|
|
|
if (imageCount('audio') + pendingVoices.value.length >= VOICE_LIMIT) {
|
|
|
|
|
|
photoError.value = `حداکثر ${VOICE_LIMIT} صوت برای هر فعالیت.`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
pendingVoices.value = [
|
|
|
|
|
|
...pendingVoices.value,
|
|
|
|
|
|
{ localId: crypto.randomUUID(), name: file.name, url: URL.createObjectURL(file), file },
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function toggleVoiceNote() {
|
|
|
|
|
|
photoError.value = '';
|
|
|
|
|
|
if (voiceNote) {
|
|
|
|
|
|
const handle = voiceNote;
|
|
|
|
|
|
voiceNote = null;
|
|
|
|
|
|
voiceNoteOn.value = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
addVoiceNote(await handle.stop());
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
photoError.value = error instanceof Error ? error.message : 'ضبط صوت ناموفق بود.';
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
voiceNoteSeconds.value = 0;
|
|
|
|
|
|
voiceNote = await startVoiceRecorder({
|
|
|
|
|
|
onTick: (seconds) => {
|
|
|
|
|
|
voiceNoteSeconds.value = seconds;
|
|
|
|
|
|
},
|
|
|
|
|
|
onAutoStop: (file) => {
|
|
|
|
|
|
voiceNote = null;
|
|
|
|
|
|
voiceNoteOn.value = false;
|
|
|
|
|
|
addVoiceNote(file);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
voiceNoteOn.value = true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
photoError.value = error instanceof Error ? error.message : 'ضبط صوت ناموفق بود.';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadQueue(items: Array<{ localId: string; name: string; file: File }>, drop: (localId: string) => void) {
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
|
const form = new FormData();
|
|
|
|
|
|
form.append('file', item.file, item.name);
|
|
|
|
|
|
await api.post(`/activities/${props.activity.id}/images`, form);
|
|
|
|
|
|
drop(item.localId);
|
|
|
|
|
|
uploadedCount.value += 1;
|
|
|
|
|
|
}
|
2026-10-27 21:52:37 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadPendingPhotos() {
|
2026-11-02 16:34:03 +03:30
|
|
|
|
await uploadQueue([...pendingPhotos.value], (localId) => {
|
|
|
|
|
|
pendingPhotos.value = pendingPhotos.value.filter((item) => item.localId !== localId);
|
|
|
|
|
|
});
|
|
|
|
|
|
await uploadQueue([...pendingVoices.value], (localId) => {
|
|
|
|
|
|
const voice = pendingVoices.value.find((item) => item.localId === localId);
|
|
|
|
|
|
if (voice) URL.revokeObjectURL(voice.url);
|
|
|
|
|
|
pendingVoices.value = pendingVoices.value.filter((item) => item.localId !== localId);
|
|
|
|
|
|
});
|
|
|
|
|
|
await uploadQueue([...pendingFiles.value], (localId) => {
|
|
|
|
|
|
pendingFiles.value = pendingFiles.value.filter((item) => item.localId !== localId);
|
|
|
|
|
|
});
|
2026-10-27 21:52:37 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submit() {
|
2026-09-02 21:29:00 +03:30
|
|
|
|
const text = comment.value.trim();
|
2026-10-27 21:52:37 +03:30
|
|
|
|
if (!canSubmit.value) return;
|
|
|
|
|
|
photoError.value = '';
|
|
|
|
|
|
uploading.value = true;
|
2026-11-02 16:34:03 +03:30
|
|
|
|
const hadPhoto =
|
|
|
|
|
|
pendingPhotos.value.length > 0 ||
|
|
|
|
|
|
pendingVoices.value.length > 0 ||
|
|
|
|
|
|
pendingFiles.value.length > 0 ||
|
|
|
|
|
|
uploadedCount.value > 0;
|
2026-10-27 21:52:37 +03:30
|
|
|
|
try {
|
|
|
|
|
|
await uploadPendingPhotos();
|
|
|
|
|
|
await props.save({ text, photoOnly: hadPhoto && !text });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
photoError.value =
|
|
|
|
|
|
pendingPhotos.value.length > 0
|
2026-11-02 16:34:03 +03:30
|
|
|
|
? apiMessage(error, 'افزودن فایل ناموفق بود.')
|
2026-10-27 21:52:37 +03:30
|
|
|
|
: apiMessage(error, 'ثبت نظر ناموفق بود.');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
uploading.value = false;
|
|
|
|
|
|
}
|
2026-09-02 21:29:00 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function close() {
|
|
|
|
|
|
emit('close');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function insertMention(name: string) {
|
|
|
|
|
|
const token = `@${name} `;
|
|
|
|
|
|
const el = textareaRef.value;
|
|
|
|
|
|
if (!el) {
|
|
|
|
|
|
comment.value += token;
|
|
|
|
|
|
mentionOpen.value = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const start = el.selectionStart ?? comment.value.length;
|
|
|
|
|
|
const end = el.selectionEnd ?? start;
|
|
|
|
|
|
comment.value = `${comment.value.slice(0, start)}${token}${comment.value.slice(end)}`;
|
|
|
|
|
|
mentionOpen.value = false;
|
|
|
|
|
|
void nextTick(() => {
|
|
|
|
|
|
el.focus();
|
|
|
|
|
|
const pos = start + token.length;
|
|
|
|
|
|
el.setSelectionRange(pos, pos);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onKeydown(ev: KeyboardEvent) {
|
|
|
|
|
|
if (ev.key === 'Escape') {
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
if (mentionOpen.value) {
|
|
|
|
|
|
mentionOpen.value = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-03 14:20:55 +03:30
|
|
|
|
if (recording.value) return;
|
2026-09-02 21:29:00 +03:30
|
|
|
|
close();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
|
|
window.addEventListener('keydown', onKeydown);
|
2026-10-11 16:48:37 +03:30
|
|
|
|
clockTimer = setInterval(() => {
|
|
|
|
|
|
now.value = new Date();
|
|
|
|
|
|
}, 30_000);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
await nextTick();
|
|
|
|
|
|
textareaRef.value?.focus();
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [{ data: org }, usersRes] = await Promise.all([
|
|
|
|
|
|
api.get<OrganizationDto>('/organizations/current'),
|
|
|
|
|
|
api.get<OrgUserDto[]>('/users').catch(() => ({ data: [] as OrgUserDto[] })),
|
|
|
|
|
|
]);
|
|
|
|
|
|
if (org.name) orgName.value = org.name;
|
|
|
|
|
|
users.value = usersRes.data;
|
|
|
|
|
|
if (!users.value.length && props.activity.assignee) {
|
|
|
|
|
|
users.value = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: props.activity.assignee.id,
|
|
|
|
|
|
email: '',
|
|
|
|
|
|
fullName: props.activity.assignee.fullName,
|
|
|
|
|
|
role: 'sales',
|
|
|
|
|
|
createdAt: '',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
orgName.value = 'سازمان';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
2026-11-02 16:34:03 +03:30
|
|
|
|
voiceNote?.cancel();
|
|
|
|
|
|
for (const voice of pendingVoices.value) URL.revokeObjectURL(voice.url);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
document.body.style.overflow = '';
|
|
|
|
|
|
window.removeEventListener('keydown', onKeydown);
|
2026-10-11 16:48:37 +03:30
|
|
|
|
clearInterval(clockTimer);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
|
<div class="layer" role="presentation">
|
2026-10-07 15:23:14 +03:30
|
|
|
|
<div class="backdrop" aria-hidden="true" />
|
2026-09-02 21:29:00 +03:30
|
|
|
|
<section
|
|
|
|
|
|
class="sheet"
|
|
|
|
|
|
role="dialog"
|
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
|
aria-labelledby="comment-sheet-title"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="handle" aria-hidden="true" />
|
|
|
|
|
|
|
|
|
|
|
|
<header class="head">
|
|
|
|
|
|
<button class="icon" type="button" aria-label="ثبت نظر" :disabled="!canSubmit" @click="submit">
|
|
|
|
|
|
<UiIcon name="check-plain" :size="22" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<h2 id="comment-sheet-title">ثبت نظر</h2>
|
|
|
|
|
|
<button class="icon" type="button" aria-label="بستن" @click="close">
|
|
|
|
|
|
<UiIcon name="x" :size="20" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="body">
|
|
|
|
|
|
<article class="ctx">
|
|
|
|
|
|
<div class="ctx-copy">
|
|
|
|
|
|
<h3>{{ activity.title }}</h3>
|
|
|
|
|
|
<p v-if="subtitle" class="sub">
|
|
|
|
|
|
<UiIcon name="building" :size="14" />
|
|
|
|
|
|
{{ subtitle }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="meta">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<UiIcon name="user" :size="14" />
|
|
|
|
|
|
{{ activity.assignee?.fullName || 'بدون مسئول' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<i />
|
2026-10-11 16:48:37 +03:30
|
|
|
|
<span title="موعد فعالیت">
|
2026-09-02 21:29:00 +03:30
|
|
|
|
<UiIcon name="calendar" :size="14" />
|
2026-10-11 16:48:37 +03:30
|
|
|
|
موعد: {{ dueLabel }}
|
2026-09-02 21:29:00 +03:30
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-10-11 16:48:37 +03:30
|
|
|
|
<p class="stamp">
|
|
|
|
|
|
<UiIcon name="clock" :size="14" />
|
|
|
|
|
|
زمان ثبت نظر: {{ nowLabel }}
|
|
|
|
|
|
</p>
|
2026-09-02 21:29:00 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
<span class="type" :class="activity.type">
|
|
|
|
|
|
<UiIcon :name="typeIcon[activity.type]" :size="22" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="editor">
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
ref="textareaRef"
|
|
|
|
|
|
v-model="comment"
|
|
|
|
|
|
:disabled="recording"
|
|
|
|
|
|
rows="6"
|
|
|
|
|
|
placeholder="متن خود را بنویسید..."
|
|
|
|
|
|
/>
|
2026-10-27 21:52:37 +03:30
|
|
|
|
<p v-if="photoError" class="photo-err">{{ photoError }}</p>
|
|
|
|
|
|
<div v-if="pendingPhotos.length" class="photo-grid">
|
|
|
|
|
|
<figure v-for="photo in pendingPhotos" :key="photo.localId">
|
|
|
|
|
|
<img :src="photo.dataUrl" :alt="photo.name" />
|
|
|
|
|
|
<button type="button" aria-label="حذف عکس" :disabled="uploading" @click="removePendingPhoto(photo.localId)">
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</figure>
|
|
|
|
|
|
</div>
|
2026-11-02 16:34:03 +03:30
|
|
|
|
<div v-if="pendingVoices.length" class="voice-list">
|
|
|
|
|
|
<div v-for="voice in pendingVoices" :key="voice.localId" class="voice-item">
|
|
|
|
|
|
<audio :src="voice.url" controls preload="metadata" />
|
|
|
|
|
|
<button type="button" aria-label="حذف صوت" :disabled="uploading" @click="removePendingVoice(voice.localId)">
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ul v-if="pendingFiles.length" class="file-list">
|
|
|
|
|
|
<li v-for="file in pendingFiles" :key="file.localId">
|
|
|
|
|
|
<UiIcon name="document" :size="15" />
|
|
|
|
|
|
<span>{{ file.name }}</span>
|
|
|
|
|
|
<button type="button" aria-label="حذف فایل" :disabled="uploading" @click="removePendingFile(file.localId)">
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
2026-09-02 21:29:00 +03:30
|
|
|
|
<div class="tools">
|
2026-11-02 16:34:03 +03:30
|
|
|
|
<div class="tool-cluster">
|
2026-09-02 21:29:00 +03:30
|
|
|
|
<div class="tool-wrap">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="tool"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="منشن"
|
2026-11-02 16:34:03 +03:30
|
|
|
|
:disabled="recording || voiceNoteOn"
|
2026-09-02 21:29:00 +03:30
|
|
|
|
@click="mentionOpen = !mentionOpen"
|
|
|
|
|
|
>
|
|
|
|
|
|
@
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<ul v-if="mentionOpen" class="mentions">
|
|
|
|
|
|
<li v-if="!users.length" class="empty">کاربری برای منشن یافت نشد.</li>
|
|
|
|
|
|
<li v-for="user in users" :key="user.id">
|
|
|
|
|
|
<button type="button" @click="insertMention(user.fullName)">
|
|
|
|
|
|
{{ user.fullName }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-11-02 16:34:03 +03:30
|
|
|
|
<button
|
|
|
|
|
|
class="tool"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="پیوست"
|
|
|
|
|
|
title="پیوست"
|
|
|
|
|
|
:disabled="photoBusy || uploading || recording || voiceNoteOn"
|
|
|
|
|
|
@click="fileInput?.click()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon name="clip" :size="16" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<input ref="fileInput" class="photo-file" type="file" multiple @change="onFilesPicked" />
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="tool"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:class="{ 'voice-on': voiceNoteOn }"
|
|
|
|
|
|
:aria-label="voiceNoteOn ? 'توقف ضبط پیام صوتی' : 'ضبط پیام صوتی'"
|
|
|
|
|
|
:title="voiceNoteOn ? 'توقف ضبط پیام صوتی' : 'ضبط پیام صوتی'"
|
|
|
|
|
|
:disabled="photoBusy || uploading || recording"
|
|
|
|
|
|
@click="toggleVoiceNote"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon name="mic" :size="16" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span v-if="voiceNoteOn" class="rec-time">{{ voiceClock(voiceNoteSeconds) }}</span>
|
|
|
|
|
|
</div>
|
2026-09-02 21:29:00 +03:30
|
|
|
|
<div class="tool-wrap">
|
|
|
|
|
|
<p v-if="voiceTip && !recording" class="tip">تایپ صوتی را امتحان کنید! 🎙️✨</p>
|
2026-09-03 14:20:55 +03:30
|
|
|
|
<VoiceToTextButton
|
|
|
|
|
|
variant="toolbar"
|
|
|
|
|
|
hide-error
|
2026-11-02 16:34:03 +03:30
|
|
|
|
:disabled="saving || voiceNoteOn"
|
2026-09-03 14:20:55 +03:30
|
|
|
|
@transcript="onVoiceTranscript"
|
|
|
|
|
|
@recording="recording = $event"
|
|
|
|
|
|
@busy="onVoiceBusy"
|
|
|
|
|
|
@error="voiceError = $event"
|
|
|
|
|
|
/>
|
2026-09-02 21:29:00 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-if="voiceError" class="voice-err">{{ voiceError }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
<p class="notice">
|
|
|
|
|
|
<UiIcon name="lightbulb" :size="18" />
|
|
|
|
|
|
یادداشت شما فقط برای اعضای تیم و مدیران {{ orgName }} قابل مشاهده خواهد بود.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
2026-09-03 14:20:55 +03:30
|
|
|
|
<div class="actions">
|
2026-09-02 21:29:00 +03:30
|
|
|
|
<button class="act primary" type="button" :disabled="!canSubmit" @click="submit">
|
2026-10-27 21:52:37 +03:30
|
|
|
|
{{ recording ? 'در حال ضبط…' : uploading || saving ? 'در حال ثبت…' : 'ثبت نظر' }}
|
2026-09-02 21:29:00 +03:30
|
|
|
|
</button>
|
2026-10-27 21:52:37 +03:30
|
|
|
|
<button class="act ghost" type="button" :disabled="saving || voiceBusy || uploading" @click="close">انصراف</button>
|
2026-09-02 21:29:00 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.layer {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 90;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.backdrop {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sheet {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100dvh;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
font-family: IRANSans, Tahoma, sans-serif;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.handle {
|
|
|
|
|
|
width: 42px;
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
margin: 0.55rem auto 0.15rem;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #d4d4d8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.head {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 40px 1fr 40px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0.15rem 0.7rem 0.45rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.head h2 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
text-align: center;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
2026-09-02 21:29:00 +03:30
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.icon {
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.icon:disabled {
|
|
|
|
|
|
opacity: 0.35;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
padding: 0.35rem 1rem calc(1rem + env(safe-area-inset-bottom));
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
align-content: start;
|
|
|
|
|
|
gap: 0.85rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ctx {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.8rem;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: 0.9rem 0.95rem;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type {
|
|
|
|
|
|
width: 46px;
|
|
|
|
|
|
height: 46px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type.call {
|
|
|
|
|
|
background: #0d9488;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type.meeting {
|
|
|
|
|
|
background: #7c3aed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type.task {
|
|
|
|
|
|
background: #ea580c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type.note {
|
|
|
|
|
|
background: #2563eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ctx-copy {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ctx-copy h3 {
|
|
|
|
|
|
margin: 0.1rem 0 0;
|
|
|
|
|
|
font-size: 0.98rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
font-weight: 600;
|
2026-09-02 21:29:00 +03:30
|
|
|
|
line-height: 1.55;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sub,
|
|
|
|
|
|
.meta span {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.3rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sub {
|
|
|
|
|
|
margin: 0.28rem 0 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.7rem;
|
|
|
|
|
|
margin-top: 0.7rem;
|
|
|
|
|
|
padding-top: 0.65rem;
|
|
|
|
|
|
border-top: 1px solid #f1f5f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 16:48:37 +03:30
|
|
|
|
.stamp {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.3rem;
|
|
|
|
|
|
margin: 0.55rem 0 0;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-02 21:29:00 +03:30
|
|
|
|
.meta i {
|
|
|
|
|
|
width: 1px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
background: #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor {
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
background: #fff;
|
|
|
|
|
|
min-height: 11.5rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor textarea {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 8.5rem;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
resize: none;
|
|
|
|
|
|
padding: 0.9rem 0.95rem 0.2rem;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
|
line-height: 1.9;
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor textarea:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor textarea::placeholder {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 21:52:37 +03:30
|
|
|
|
.photo-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.45rem;
|
|
|
|
|
|
padding: 0 0.7rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-row small {
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-add {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
border: 1px dashed #99f6e4;
|
|
|
|
|
|
background: #f0fdfa;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
|
padding: 0.35rem 0.7rem;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-add:disabled {
|
|
|
|
|
|
opacity: 0.55;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-file {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-err {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0 0.7rem;
|
|
|
|
|
|
color: #b91c1c;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-grid {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 0.45rem;
|
|
|
|
|
|
padding: 0 0.7rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-grid figure {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
width: 4.5rem;
|
|
|
|
|
|
height: 4.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-grid img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
border-radius: 0.6rem;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.photo-grid figure button {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0.15rem;
|
|
|
|
|
|
inset-inline-end: 0.15rem;
|
|
|
|
|
|
width: 1.2rem;
|
|
|
|
|
|
height: 1.2rem;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: rgba(15, 23, 42, 0.72);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-02 21:29:00 +03:30
|
|
|
|
.tools {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
padding: 0.35rem 0.5rem 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-02 16:34:03 +03:30
|
|
|
|
.tool-cluster {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tool.voice-on {
|
|
|
|
|
|
border-color: #fecaca;
|
|
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
color: #b91c1c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rec-time {
|
|
|
|
|
|
color: #b91c1c;
|
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-list {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
padding: 0 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-item audio {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
height: 2.1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-item button,
|
|
|
|
|
|
.file-list button {
|
|
|
|
|
|
width: 1.4rem;
|
|
|
|
|
|
height: 1.4rem;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #e2e8f0;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-list {
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0 0.5rem;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.3rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-list li {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-list span {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-02 21:29:00 +03:30
|
|
|
|
.tool-wrap {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tool {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
font: inherit;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
font-weight: 600;
|
2026-09-02 21:29:00 +03:30
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 15:48:28 +03:30
|
|
|
|
.tool-wrap :deep(.vtt:not(.on):not(.busy)) {
|
2026-09-02 21:29:00 +03:30
|
|
|
|
color: #0d9488;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tip {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset-inline-end: 0;
|
|
|
|
|
|
bottom: calc(100% + 8px);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
background: #111827;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 0.74rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
padding: 0.42rem 0.65rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tip::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset-inline-end: 14px;
|
|
|
|
|
|
top: 100%;
|
|
|
|
|
|
border: 6px solid transparent;
|
|
|
|
|
|
border-top-color: #111827;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mentions {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset-inline-start: 0;
|
|
|
|
|
|
bottom: calc(100% + 6px);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0.3rem;
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
min-width: 11.5rem;
|
|
|
|
|
|
max-height: 12rem;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
background: #fff;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.16);
|
|
|
|
|
|
z-index: 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mentions button,
|
|
|
|
|
|
.mentions .empty {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
text-align: start;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
padding: 0.55rem 0.65rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mentions .empty {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mentions button:hover {
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-err {
|
|
|
|
|
|
margin: -0.35rem 0 0;
|
|
|
|
|
|
color: #b91c1c;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notice {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
padding: 0.72rem 0.8rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
background: #ecfdf5;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notice :deep(.ico) {
|
|
|
|
|
|
margin-top: 0.12rem;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.actions {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 0.65rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.act {
|
2026-09-10 23:45:11 +03:30
|
|
|
|
min-height: var(--control-h);
|
|
|
|
|
|
height: var(--control-h);
|
|
|
|
|
|
border-radius: var(--radius);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
font: inherit;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
font-weight: 600;
|
2026-09-02 21:29:00 +03:30
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.act.primary {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: #14b8a6;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.act.ghost {
|
|
|
|
|
|
border: 1.5px solid #14b8a6;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.act:disabled {
|
|
|
|
|
|
opacity: 0.45;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 900px) {
|
|
|
|
|
|
.layer {
|
|
|
|
|
|
background: rgba(15, 23, 42, 0.28);
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 1.2rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.backdrop {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sheet {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
max-width: 480px;
|
|
|
|
|
|
height: min(92dvh, 860px);
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius, 10px);
|
2026-09-02 21:29:00 +03:30
|
|
|
|
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|