1003 lines
24 KiB
Vue
1003 lines
24 KiB
Vue
<template>
|
||
<div class="admin-composer">
|
||
<div class="admin-composer__container">
|
||
<div
|
||
class="admin-composer__panel"
|
||
:class="{ 'admin-composer__panel--drag': isDragOver }"
|
||
@dragenter.prevent="isDragOver = true"
|
||
@dragover.prevent="isDragOver = true"
|
||
@dragleave.prevent="onDragLeave"
|
||
@drop.prevent="onDrop"
|
||
>
|
||
<input
|
||
ref="fileInputRef"
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
class="hidden"
|
||
@change="onFileChange"
|
||
/>
|
||
|
||
<div v-if="attachments.length" class="admin-composer__attachments">
|
||
<div
|
||
v-for="(attachment, index) in attachments"
|
||
:key="attachment.id"
|
||
draggable="true"
|
||
class="admin-composer__attachment group"
|
||
:class="
|
||
attachment.type === 'image'
|
||
? 'admin-composer__attachment--image'
|
||
: 'admin-composer__attachment--audio'
|
||
"
|
||
@dragstart="onPreviewDragStart(index)"
|
||
@dragover.prevent
|
||
@drop.prevent="onPreviewDrop(index)"
|
||
>
|
||
<template v-if="attachment.type === 'image'">
|
||
<img
|
||
:src="attachment.url"
|
||
:alt="attachment.name"
|
||
class="admin-composer__attachment-image"
|
||
/>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<div class="admin-composer__audio-badge">
|
||
<BaseIcon icon="vuesax-linear:microphone-2" size="18" />
|
||
</div>
|
||
|
||
<div class="min-w-0 flex-1">
|
||
<div class="truncate text-sm font-semibold text-zinc-800">
|
||
{{ attachment.name }}
|
||
</div>
|
||
<audio :src="attachment.url" controls class="mt-2 block w-full max-w-[160px]" />
|
||
</div>
|
||
</template>
|
||
|
||
<BaseIcon
|
||
icon="vuesax-linear:close-circle"
|
||
@click="removeAttachment(attachment.id)"
|
||
class="admin-composer__remove cursor-pointer"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-show="showRecorder || isRecording || recordedBlobUrl"
|
||
class="admin-composer__recorder"
|
||
>
|
||
<div class="admin-composer__recorder-head">
|
||
<div class="flex items-center justify-between gap-3">
|
||
<div class="flex items-center gap-2">
|
||
<span
|
||
v-if="isRecording"
|
||
class="h-2.5 w-2.5 rounded-full bg-red-500 animate-pulse"
|
||
/>
|
||
<span class="text-sm font-semibold text-zinc-800">
|
||
{{ isRecording ? 'در حال ضبط وویس...' : 'پیشنمایش وویس' }}
|
||
</span>
|
||
</div>
|
||
|
||
<span class="text-xs text-zinc-500">
|
||
{{ formattedRecordTime }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="px-3 py-3">
|
||
<div ref="waveformEl" class="min-h-[64px]" />
|
||
</div>
|
||
|
||
<div v-if="recordedBlobUrl && !isRecording" class="admin-composer__recorder-actions">
|
||
<BaseButton type="default" class="admin-secondary-btn" @click="discardRecordedVoice">
|
||
حذف وویس
|
||
</BaseButton>
|
||
|
||
<BaseButton
|
||
type="primary"
|
||
class="admin-primary-btn"
|
||
@click="appendRecordedVoiceToAttachments"
|
||
>
|
||
افزودن به پاسخ
|
||
</BaseButton>
|
||
</div>
|
||
</div>
|
||
<div v-if="replyTo" class="admin-composer__reply-preview">
|
||
<div class="admin-composer__reply-line" />
|
||
<div v-if="replyTo.attachment?.type === 'image'" class="admin-composer__reply-thumb">
|
||
<img :src="replyTo.attachment.url" :alt="replyTo.attachment.name || 'reply image'" />
|
||
</div>
|
||
<div class="admin-composer__reply-content">
|
||
<div class="admin-composer__reply-title">پاسخ به {{ replySenderLabel }}</div>
|
||
|
||
<div class="admin-composer__reply-text">
|
||
{{ replyPreviewText }}
|
||
</div>
|
||
</div>
|
||
|
||
<button type="button" class="admin-composer__reply-close" @click="$emit('cancel-reply')">
|
||
×
|
||
</button>
|
||
</div>
|
||
<div class="admin-composer__row">
|
||
<div class="admin-composer__input-shell">
|
||
<BaseButton
|
||
type="link"
|
||
class="admin-attach-btn"
|
||
:disabled="disabled || isRecording"
|
||
@click="openFilePicker"
|
||
>
|
||
<BaseIcon icon="vuesax-linear:gallery" size="20" />
|
||
</BaseButton>
|
||
<textarea
|
||
ref="textareaRef"
|
||
:value="inputModel"
|
||
:disabled="disabled || isRecording"
|
||
rows="1"
|
||
placeholder="پیامت را بنویس..."
|
||
class="admin-composer-textarea w-full resize-none overflow-y-auto bg-transparent px-2 py-[10px] text-sm leading-7 outline-none !text-[16px]"
|
||
@input="onInput"
|
||
@keydown="onComposerKeydown"
|
||
/>
|
||
</div>
|
||
|
||
<div class="admin-composer__actions !p-0">
|
||
<BaseButton
|
||
type="primary"
|
||
icon="vuesax-linear:direct-right"
|
||
class="admin-send-btn !p-0"
|
||
:disabled="
|
||
disabled || isRecording || (!inputModel.trim() && attachments.length === 0)
|
||
"
|
||
@click="submit"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isDragOver" class="admin-composer__dropzone">عکس را اینجا رها کن</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||
import WaveSurfer from 'wavesurfer.js'
|
||
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.esm.js'
|
||
import type { ComposerSendPayload, SupportAttachment, SupportMessageReplyTo } from '@/model/support'
|
||
|
||
const props = defineProps<{
|
||
value?: string
|
||
modelValue?: string
|
||
disabled?: boolean
|
||
replyTo?: SupportMessageReplyTo | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', value: string): void
|
||
(e: 'send', payload: ComposerSendPayload): void
|
||
(e: 'cancel-reply'): void
|
||
}>()
|
||
|
||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||
const waveformEl = ref<HTMLElement | null>(null)
|
||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||
|
||
const shouldRefocusAfterSend = ref(false)
|
||
|
||
const attachments = ref<SupportAttachment[]>([])
|
||
const isDragOver = ref(false)
|
||
const draggedPreviewIndex = ref<number | null>(null)
|
||
|
||
const showRecorder = ref(false)
|
||
const isRecording = ref(false)
|
||
const recordTimeMs = ref(0)
|
||
const recordedBlob = ref<Blob | null>(null)
|
||
const recordedBlobUrl = ref('')
|
||
const recordedMimeType = ref('audio/webm')
|
||
|
||
let wavesurfer: WaveSurfer | null = null
|
||
let recordPlugin: ReturnType<typeof RecordPlugin.create> | null = null
|
||
|
||
const createId = () => `${Date.now()}-${Math.floor(Math.random() * 100000)}`
|
||
|
||
const inputModel = ref(String(props.modelValue ?? ''))
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
async (value) => {
|
||
const normalized = String(value ?? '')
|
||
if (normalized !== inputModel.value) {
|
||
inputModel.value = normalized
|
||
}
|
||
|
||
await nextTick()
|
||
resizeTextarea()
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
const formattedRecordTime = computed(() => {
|
||
const totalSeconds = Math.floor(recordTimeMs.value / 1000)
|
||
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0')
|
||
const seconds = String(totalSeconds % 60).padStart(2, '0')
|
||
return `${minutes}:${seconds}`
|
||
})
|
||
|
||
const replySenderLabel = computed(() => {
|
||
if (props.replyTo?.sender === 'admin') return 'شما'
|
||
if (props.replyTo?.sender === 'user') return 'کاربر'
|
||
return 'پیام'
|
||
})
|
||
|
||
const replyPreviewText = computed(() => {
|
||
return String(props.replyTo?.text || 'پیام').trim()
|
||
})
|
||
|
||
const resizeTextarea = () => {
|
||
const el = textareaRef.value
|
||
if (!el) return
|
||
|
||
el.style.height = 'auto'
|
||
|
||
const nextHeight = Math.min(el.scrollHeight, 144)
|
||
el.style.height = `${nextHeight}px`
|
||
el.style.overflowY = el.scrollHeight > 144 ? 'auto' : 'hidden'
|
||
}
|
||
|
||
const focusTextarea = async () => {
|
||
await nextTick()
|
||
|
||
const el = textareaRef.value
|
||
if (!el || props.disabled || isRecording.value) return
|
||
|
||
el.focus({ preventScroll: true })
|
||
|
||
const length = el.value.length
|
||
el.setSelectionRange(length, length)
|
||
}
|
||
|
||
const onInput = (event: Event) => {
|
||
const target = event.target as HTMLTextAreaElement
|
||
inputModel.value = target.value
|
||
emit('update:modelValue', target.value)
|
||
resizeTextarea()
|
||
}
|
||
|
||
const onComposerKeydown = (event: KeyboardEvent) => {
|
||
if ((event as KeyboardEvent & { isComposing?: boolean }).isComposing || event.keyCode === 229) {
|
||
return
|
||
}
|
||
|
||
if (event.key === 'Enter' && !event.shiftKey) {
|
||
event.preventDefault()
|
||
submit()
|
||
}
|
||
}
|
||
|
||
const openFilePicker = () => {
|
||
fileInputRef.value?.click()
|
||
}
|
||
|
||
const createImageAttachment = (file: File): SupportAttachment => ({
|
||
id: createId(),
|
||
type: 'image',
|
||
url: URL.createObjectURL(file),
|
||
name: file.name,
|
||
size: file.size,
|
||
mimeType: file.type,
|
||
file
|
||
})
|
||
|
||
const createAudioAttachment = (file: File, durationMs?: number): SupportAttachment => ({
|
||
id: createId(),
|
||
type: 'audio',
|
||
url: URL.createObjectURL(file),
|
||
name: file.name,
|
||
size: file.size,
|
||
mimeType: file.type,
|
||
durationMs,
|
||
file
|
||
})
|
||
|
||
const addImageFiles = (files: File[]) => {
|
||
const imageFiles = files.filter((file) => file.type.startsWith('image/'))
|
||
if (!imageFiles.length) return
|
||
attachments.value.push(...imageFiles.map(createImageAttachment))
|
||
}
|
||
|
||
const onFileChange = (event: Event) => {
|
||
const target = event.target as HTMLInputElement
|
||
const files = Array.from(target.files ?? [])
|
||
addImageFiles(files)
|
||
|
||
if (fileInputRef.value) {
|
||
fileInputRef.value.value = ''
|
||
}
|
||
}
|
||
|
||
const onDrop = (event: DragEvent) => {
|
||
isDragOver.value = false
|
||
const files = Array.from(event.dataTransfer?.files ?? [])
|
||
addImageFiles(files)
|
||
}
|
||
|
||
const onDragLeave = (event: DragEvent) => {
|
||
const target = event.currentTarget as HTMLElement | null
|
||
const relatedTarget = event.relatedTarget as Node | null
|
||
if (!target || (relatedTarget && target.contains(relatedTarget))) return
|
||
isDragOver.value = false
|
||
}
|
||
|
||
const removeAttachment = (id: string) => {
|
||
const target = attachments.value.find((item) => item.id === id)
|
||
if (target?.url.startsWith('blob:')) {
|
||
URL.revokeObjectURL(target.url)
|
||
}
|
||
attachments.value = attachments.value.filter((item) => item.id !== id)
|
||
}
|
||
|
||
const onPreviewDragStart = (index: number) => {
|
||
draggedPreviewIndex.value = index
|
||
}
|
||
|
||
const onPreviewDrop = (dropIndex: number) => {
|
||
if (draggedPreviewIndex.value === null) return
|
||
if (draggedPreviewIndex.value === dropIndex) return
|
||
|
||
const items = [...attachments.value]
|
||
const movedItem = items[draggedPreviewIndex.value]
|
||
|
||
items.splice(draggedPreviewIndex.value, 1)
|
||
items.splice(dropIndex, 0, movedItem)
|
||
|
||
attachments.value = items
|
||
draggedPreviewIndex.value = null
|
||
}
|
||
|
||
const requestMicrophonePermission = async () => {
|
||
const hasMediaDevices =
|
||
typeof navigator !== 'undefined' &&
|
||
!!navigator.mediaDevices &&
|
||
typeof navigator.mediaDevices.getUserMedia === 'function'
|
||
|
||
if (!hasMediaDevices) {
|
||
alert('ضبط وویس فقط روی localhost یا https کار میکند.')
|
||
return false
|
||
}
|
||
|
||
try {
|
||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||
stream.getTracks().forEach((track) => track.stop())
|
||
return true
|
||
} catch (error) {
|
||
console.error('mic permission failed:', error)
|
||
alert('دسترسی میکروفون گرفته نشد.')
|
||
return false
|
||
}
|
||
}
|
||
|
||
const initWaveSurfer = async () => {
|
||
if (!waveformEl.value || wavesurfer) return
|
||
|
||
recordPlugin = RecordPlugin.create({
|
||
scrollingWaveform: true,
|
||
renderRecordedAudio: true,
|
||
mediaRecorderTimeslice: 250,
|
||
mimeType: 'audio/webm'
|
||
})
|
||
|
||
wavesurfer = WaveSurfer.create({
|
||
container: waveformEl.value,
|
||
height: 64,
|
||
cursorWidth: 0,
|
||
barWidth: 3,
|
||
barGap: 2,
|
||
barRadius: 999,
|
||
normalize: true,
|
||
interact: true,
|
||
plugins: [recordPlugin]
|
||
})
|
||
|
||
recordPlugin.on('record-progress', (duration) => {
|
||
recordTimeMs.value = duration
|
||
})
|
||
|
||
recordPlugin.on('record-start', () => {
|
||
isRecording.value = true
|
||
recordTimeMs.value = 0
|
||
recordedBlob.value = null
|
||
|
||
if (recordedBlobUrl.value) {
|
||
URL.revokeObjectURL(recordedBlobUrl.value)
|
||
recordedBlobUrl.value = ''
|
||
}
|
||
})
|
||
|
||
recordPlugin.on('record-end', (blob) => {
|
||
isRecording.value = false
|
||
recordedBlob.value = blob
|
||
recordedMimeType.value = blob.type || 'audio/webm'
|
||
|
||
if (recordedBlobUrl.value) {
|
||
URL.revokeObjectURL(recordedBlobUrl.value)
|
||
}
|
||
|
||
recordedBlobUrl.value = URL.createObjectURL(blob)
|
||
})
|
||
}
|
||
|
||
const toggleRecording = async () => {
|
||
if (props.disabled) return
|
||
|
||
const granted = await requestMicrophonePermission()
|
||
if (!granted) return
|
||
|
||
try {
|
||
if (!showRecorder.value) {
|
||
showRecorder.value = true
|
||
await nextTick()
|
||
}
|
||
|
||
await initWaveSurfer()
|
||
if (!recordPlugin) return
|
||
|
||
if (recordPlugin.isRecording()) {
|
||
recordPlugin.stopRecording()
|
||
} else {
|
||
await recordPlugin.startRecording()
|
||
}
|
||
} catch (error) {
|
||
console.error('Voice recording failed:', error)
|
||
alert('ضبط وویس شروع نشد.')
|
||
}
|
||
}
|
||
|
||
const discardRecordedVoice = () => {
|
||
recordedBlob.value = null
|
||
recordTimeMs.value = 0
|
||
isRecording.value = false
|
||
|
||
if (recordedBlobUrl.value) {
|
||
URL.revokeObjectURL(recordedBlobUrl.value)
|
||
recordedBlobUrl.value = ''
|
||
}
|
||
|
||
showRecorder.value = false
|
||
wavesurfer?.empty()
|
||
}
|
||
|
||
const appendRecordedVoiceToAttachments = () => {
|
||
if (!recordedBlob.value) return
|
||
|
||
const extension = recordedMimeType.value.includes('ogg')
|
||
? 'ogg'
|
||
: recordedMimeType.value.includes('mp4')
|
||
? 'm4a'
|
||
: 'webm'
|
||
|
||
const file = new File([recordedBlob.value], `voice-${Date.now()}.${extension}`, {
|
||
type: recordedMimeType.value || 'audio/webm'
|
||
})
|
||
|
||
attachments.value.push(createAudioAttachment(file, recordTimeMs.value))
|
||
discardRecordedVoice()
|
||
}
|
||
|
||
const clearComposerAttachments = () => {
|
||
attachments.value.forEach((item) => {
|
||
if (item.url.startsWith('blob:')) {
|
||
URL.revokeObjectURL(item.url)
|
||
}
|
||
})
|
||
attachments.value = []
|
||
}
|
||
|
||
const resetComposer = () => {
|
||
inputModel.value = ''
|
||
emit('update:modelValue', '')
|
||
clearComposerAttachments()
|
||
}
|
||
|
||
const submit = async () => {
|
||
const text = inputModel.value.trim()
|
||
|
||
if (!text && attachments.value.length === 0) return
|
||
if (props.disabled || isRecording.value) return
|
||
|
||
const payloadAttachments = attachments.value.map((item) => ({
|
||
...item
|
||
}))
|
||
|
||
shouldRefocusAfterSend.value = true
|
||
|
||
resetComposer()
|
||
await nextTick()
|
||
resizeTextarea()
|
||
|
||
emit('send', {
|
||
text,
|
||
attachments: payloadAttachments,
|
||
replyTo: props.replyTo || null
|
||
})
|
||
|
||
await nextTick()
|
||
|
||
if (!props.disabled) {
|
||
shouldRefocusAfterSend.value = false
|
||
|
||
requestAnimationFrame(() => {
|
||
focusTextarea()
|
||
resizeTextarea()
|
||
})
|
||
}
|
||
}
|
||
|
||
onBeforeUnmount(() => {
|
||
clearComposerAttachments()
|
||
|
||
if (recordedBlobUrl.value) {
|
||
URL.revokeObjectURL(recordedBlobUrl.value)
|
||
}
|
||
|
||
recordPlugin?.destroy()
|
||
wavesurfer?.destroy()
|
||
recordPlugin = null
|
||
wavesurfer = null
|
||
})
|
||
|
||
watch(
|
||
() => props.disabled,
|
||
async (disabled, oldDisabled) => {
|
||
if (oldDisabled && !disabled && shouldRefocusAfterSend.value) {
|
||
shouldRefocusAfterSend.value = false
|
||
|
||
await nextTick()
|
||
|
||
requestAnimationFrame(() => {
|
||
focusTextarea()
|
||
resizeTextarea()
|
||
})
|
||
}
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style lang="less">
|
||
.admin-composer {
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
z-index: 2;
|
||
background: var(--admin-bg-elevated, #fff);
|
||
border-top: 1px solid var(--admin-border, #e5e7eb);
|
||
}
|
||
|
||
.admin-composer__container {
|
||
max-width: 64rem;
|
||
margin: 0 auto;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.admin-composer__panel {
|
||
border: 1px solid var(--admin-border-strong, #d9e5ff);
|
||
border-radius: 16px;
|
||
background: var(--admin-bg-muted, #f5f8ff);
|
||
padding: 10px;
|
||
transition: 0.2s ease;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.admin-composer__panel--drag {
|
||
border-color: var(--admin-gold-light, #9db8ff);
|
||
background: color-mix(in srgb, var(--admin-gold-light, #2563eb) 12%, var(--admin-bg-muted, #eef4ff));
|
||
}
|
||
|
||
.admin-composer__reply-preview {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--admin-border, #cfe0ff);
|
||
border-radius: 14px;
|
||
background: var(--admin-bg-elevated, #fff);
|
||
direction: rtl;
|
||
}
|
||
|
||
.admin-composer__reply-line {
|
||
width: 4px;
|
||
height: 36px;
|
||
flex-shrink: 0;
|
||
border-radius: 999px;
|
||
background: var(--colorPrimary, #2563eb);
|
||
}
|
||
|
||
.admin-composer__reply-content {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.admin-composer__reply-title {
|
||
margin-bottom: 2px;
|
||
font-size: 12px;
|
||
font-weight: 900;
|
||
color: var(--colorPrimary, #2563eb);
|
||
}
|
||
|
||
.admin-composer__reply-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 60vw;
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
color: var(--admin-text-muted, #52525b);
|
||
}
|
||
|
||
.admin-composer__reply-close {
|
||
width: 28px;
|
||
height: 28px;
|
||
flex-shrink: 0;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
background: var(--admin-bg-muted, #eef2ff);
|
||
color: #334155;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.admin-composer__attachments {
|
||
display: flex;
|
||
gap: 8px;
|
||
overflow-x: auto;
|
||
padding-bottom: 4px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.admin-composer__attachment {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
overflow: hidden;
|
||
border-radius: 16px;
|
||
border: 1px solid #dbe3f0;
|
||
background: #fff;
|
||
}
|
||
|
||
.admin-composer__attachment--image {
|
||
width: 88px;
|
||
height: 88px;
|
||
}
|
||
|
||
.admin-composer__attachment--audio {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 240px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.admin-composer__attachment-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.admin-composer__audio-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 14px;
|
||
background: #18181b;
|
||
color: #fff;
|
||
}
|
||
|
||
.admin-composer__remove {
|
||
position: absolute;
|
||
top: 6px;
|
||
right: 6px;
|
||
background: rgba(239, 68, 68, 0.92);
|
||
color: #fff;
|
||
padding: 2px;
|
||
border-radius: 9999px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.admin-composer__recorder {
|
||
margin-bottom: 10px;
|
||
overflow: hidden;
|
||
border-radius: 16px;
|
||
border: 1px solid #dbe3f0;
|
||
background: #fff;
|
||
}
|
||
|
||
.admin-composer__recorder-head {
|
||
border-bottom: 1px solid #eef2f7;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.admin-composer__recorder-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
border-top: 1px solid #eef2f7;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.admin-composer__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
direction: ltr;
|
||
}
|
||
|
||
.admin-composer__actions {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.admin-composer__input-shell {
|
||
min-width: 0;
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
border: 1px solid var(--admin-border, #cfe0ff);
|
||
border-radius: 14px;
|
||
background: var(--admin-bg-elevated, #fff);
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.admin-composer__dropzone {
|
||
margin-top: 10px;
|
||
border: 1px dashed var(--admin-border-strong, #cbd5e1);
|
||
border-radius: 14px;
|
||
background: color-mix(in srgb, var(--admin-bg-elevated, #fff) 88%, transparent);
|
||
padding: 12px 16px;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
color: var(--admin-text-secondary, #64748b);
|
||
}
|
||
|
||
.admin-composer-textarea {
|
||
display: block;
|
||
width: 100%;
|
||
min-height: 48px;
|
||
max-height: 144px;
|
||
border: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
resize: none;
|
||
direction: rtl;
|
||
text-align: right;
|
||
color: var(--admin-text, var(--colorText, #111827));
|
||
font-family: var(--font-family, iran-sans), serif;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgb(var(--colorPrimaryRGB, 37 99 235) / 0.35) transparent;
|
||
}
|
||
|
||
.admin-composer-textarea::placeholder {
|
||
color: var(--admin-text-secondary, color-mix(in srgb, var(--colorText, #111827) 45%, transparent));
|
||
}
|
||
|
||
.admin-composer-textarea::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.admin-composer-textarea::-webkit-scrollbar-thumb {
|
||
background: rgb(var(--colorPrimaryRGB, 37 99 235) / 0.35);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.admin-composer-textarea::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.admin-composer-textarea:focus {
|
||
outline: none;
|
||
}
|
||
|
||
.admin-composer__actions {
|
||
.ant-btn {
|
||
width: 50px !important;
|
||
height: 50px !important;
|
||
min-width: 50px !important;
|
||
border-radius: 14px !important;
|
||
border: 0 !important;
|
||
color: #fff !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
.ant-btn:hover,
|
||
.ant-btn:focus {
|
||
//background: #7d9bf3 !important;
|
||
color: #fff !important;
|
||
}
|
||
|
||
.ant-btn[disabled] {
|
||
background: #c7d4fb !important;
|
||
color: #fff !important;
|
||
}
|
||
}
|
||
|
||
.admin-primary-btn {
|
||
.ant-btn {
|
||
border-radius: 14px !important;
|
||
border-color: #18181b !important;
|
||
background: #18181b !important;
|
||
color: #fff !important;
|
||
box-shadow: none !important;
|
||
}
|
||
}
|
||
|
||
.admin-secondary-btn {
|
||
.ant-btn {
|
||
border-radius: 14px !important;
|
||
border: 1px solid #e4e4e7 !important;
|
||
background: #fff !important;
|
||
color: #3f3f46 !important;
|
||
box-shadow: none !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 767px) {
|
||
.admin-composer__container {
|
||
padding: 8px 10px;
|
||
}
|
||
|
||
.admin-composer__panel {
|
||
border-radius: 14px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.admin-composer__row {
|
||
gap: 8px;
|
||
direction: ltr;
|
||
}
|
||
|
||
.admin-composer__actions {
|
||
.ant-btn {
|
||
width: 50px !important;
|
||
height: 50px !important;
|
||
min-width: 50px !important;
|
||
border-radius: 14px !important;
|
||
}
|
||
}
|
||
|
||
.admin-composer-textarea {
|
||
textarea.ant-input {
|
||
min-height: 50px;
|
||
padding: 12px 14px;
|
||
border-radius: 14px;
|
||
direction: rtl;
|
||
text-align: right;
|
||
}
|
||
}
|
||
}
|
||
|
||
.admin-composer__actions {
|
||
gap: 8px;
|
||
}
|
||
|
||
.admin-attach-btn {
|
||
width: 30px;
|
||
height: 50px;
|
||
min-width: 20px;
|
||
border: unset;
|
||
background: #fff;
|
||
color: var(--colorPrimary, #2563eb);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.admin-attach-btn:disabled {
|
||
opacity: 0.55;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.admin-composer__reply-thumb {
|
||
width: 44px;
|
||
height: 44px;
|
||
flex: 0 0 44px;
|
||
overflow: hidden;
|
||
border-radius: 12px;
|
||
background: #eef2ff;
|
||
}
|
||
|
||
.admin-composer__reply-thumb img {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.admin-composer__container {
|
||
padding: 12px 14px;
|
||
}
|
||
}
|
||
|
||
|
||
/* dark mode */
|
||
html.is-dark .admin-composer {
|
||
background: var(--admin-bg-elevated);
|
||
border-top-color: var(--admin-border);
|
||
}
|
||
|
||
html.is-dark .admin-composer__panel {
|
||
border-color: var(--admin-border-strong);
|
||
background: var(--admin-bg-muted);
|
||
}
|
||
|
||
html.is-dark .admin-composer__panel--drag {
|
||
border-color: var(--admin-gold-light);
|
||
background: color-mix(in srgb, var(--admin-gold-light) 12%, var(--admin-bg-muted));
|
||
}
|
||
|
||
html.is-dark .admin-composer__reply-preview,
|
||
html.is-dark .admin-composer__input-shell,
|
||
html.is-dark .admin-composer__attachment,
|
||
html.is-dark .admin-composer__recorder {
|
||
background: var(--admin-bg-elevated) !important;
|
||
border-color: var(--admin-border) !important;
|
||
color: var(--admin-text) !important;
|
||
}
|
||
|
||
html.is-dark .admin-composer__reply-title {
|
||
color: var(--admin-gold-light) !important;
|
||
}
|
||
|
||
html.is-dark .admin-composer__reply-text {
|
||
color: var(--admin-text-muted) !important;
|
||
}
|
||
|
||
html.is-dark .admin-composer__reply-close {
|
||
color: var(--admin-text-muted) !important;
|
||
}
|
||
|
||
html.is-dark .admin-composer-textarea {
|
||
color: var(--admin-text) !important;
|
||
}
|
||
|
||
html.is-dark .admin-composer-textarea::placeholder {
|
||
color: var(--admin-text-secondary) !important;
|
||
}
|
||
|
||
html.is-dark .admin-composer__dropzone {
|
||
background: color-mix(in srgb, var(--admin-bg-elevated) 88%, transparent);
|
||
border-color: var(--admin-border-strong);
|
||
color: var(--admin-text-secondary);
|
||
}
|
||
|
||
html.is-dark .admin-composer__audio-badge {
|
||
color: var(--admin-text);
|
||
}
|
||
|
||
html.is-dark .admin-attach-btn {
|
||
background: transparent !important;
|
||
color: var(--admin-gold-light, var(--colorPrimary, #2563eb)) !important;
|
||
}
|
||
|
||
html.is-dark .admin-attach-btn:hover {
|
||
background: color-mix(in srgb, var(--admin-gold-light, #2563eb) 12%, transparent) !important;
|
||
}
|
||
|
||
html.is-dark .admin-composer__reply-thumb {
|
||
background: var(--admin-bg-elevated);
|
||
border: 1px solid var(--admin-border);
|
||
}
|
||
|
||
html.is-dark .admin-composer__remove {
|
||
background: var(--admin-bg-elevated);
|
||
color: var(--admin-text-secondary);
|
||
border-color: var(--admin-border);
|
||
}
|
||
|
||
</style>
|