2026-08-29 21:06:30 +03:30
|
|
|
|
<script setup lang="ts">
|
2026-09-05 19:13:01 +03:30
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
|
|
|
|
import { useRoute } from 'vue-router';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
import { api } from '@/lib/api';
|
2026-09-01 14:29:34 +03:30
|
|
|
|
import NoteAiSuggestionPanel from '@/components/NoteAiSuggestionPanel.vue';
|
2026-09-02 21:29:00 +03:30
|
|
|
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
2026-09-03 14:20:55 +03:30
|
|
|
|
import VoiceToTextButton from '@/components/VoiceToTextButton.vue';
|
2026-09-05 19:13:01 +03:30
|
|
|
|
import UiIcon from '@/components/UiIcon.vue';
|
2026-09-03 14:20:55 +03:30
|
|
|
|
import { appendSpokenText } from '@/lib/spoken-text';
|
2026-09-05 19:13:01 +03:30
|
|
|
|
import {
|
|
|
|
|
|
calendarDaysBetween,
|
|
|
|
|
|
formatClockFa,
|
|
|
|
|
|
formatJalaliDay,
|
|
|
|
|
|
toFaDigits,
|
|
|
|
|
|
} from '@/lib/jalali';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
import type {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
ActivityDto,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
DealDto,
|
|
|
|
|
|
NoteAttachmentDto,
|
2026-09-05 19:13:01 +03:30
|
|
|
|
NoteAiSuggestionDto,
|
|
|
|
|
|
NoteChecklistItemDto,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
NoteDto,
|
|
|
|
|
|
NoteType,
|
|
|
|
|
|
PaginatedResponse,
|
|
|
|
|
|
PersonDto,
|
|
|
|
|
|
PipelineBoardDto,
|
|
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
type ViewMode = 'grid' | 'list';
|
|
|
|
|
|
type SortKey = 'updatedAt' | 'createdAt' | 'reminderAt';
|
|
|
|
|
|
type ColorKey = 'sand' | 'rose' | 'sky' | 'mint' | 'lavender' | 'default';
|
|
|
|
|
|
type OpenMode = 'default' | 'image' | 'mic' | 'attach' | 'audio' | 'checklist';
|
|
|
|
|
|
|
2026-09-02 21:29:00 +03:30
|
|
|
|
const route = useRoute();
|
2026-09-05 19:13:01 +03:30
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const saving = ref(false);
|
|
|
|
|
|
const error = ref('');
|
|
|
|
|
|
const q = ref('');
|
|
|
|
|
|
const typeFilter = ref<NoteType | 'all'>('all');
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const sortKey = ref<SortKey>('updatedAt');
|
|
|
|
|
|
const viewMode = ref<ViewMode>('grid');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const notes = ref<NoteDto[]>([]);
|
|
|
|
|
|
const people = ref<PersonDto[]>([]);
|
|
|
|
|
|
const deals = ref<DealDto[]>([]);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const activities = ref<ActivityDto[]>([]);
|
|
|
|
|
|
const pendingAi = ref<Record<string, boolean>>({});
|
|
|
|
|
|
const menuOpenId = ref<string | null>(null);
|
|
|
|
|
|
const blobCache = ref<Record<string, string>>({});
|
|
|
|
|
|
const audioDurations = ref<Record<string, string>>({});
|
|
|
|
|
|
|
|
|
|
|
|
const editorOpen = ref(false);
|
|
|
|
|
|
const moreSheetOpen = ref(false);
|
|
|
|
|
|
const showChecklistEditor = ref(false);
|
|
|
|
|
|
const editingId = ref<string | null>(null);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
|
|
|
|
|
const draftTitle = ref('');
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const draftBody = ref('');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const draftType = ref<NoteType>('general');
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const draftColor = ref<ColorKey>('sand');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const draftTags = ref('');
|
|
|
|
|
|
const draftPersonId = ref('');
|
|
|
|
|
|
const draftDealId = ref('');
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const draftActivityId = ref('');
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const draftReminder = ref('');
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const draftLocation = ref('');
|
|
|
|
|
|
const draftAnalyze = ref(true);
|
2026-09-05 19:41:39 +03:30
|
|
|
|
const draftPinned = ref(false);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const draftChecklist = ref<NoteChecklistItemDto[]>([]);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const draftFiles = ref<File[]>([]);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const existingAttachments = ref<NoteAttachmentDto[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
const imageInput = ref<HTMLInputElement | null>(null);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const fileInput = ref<HTMLInputElement | null>(null);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const audioInput = ref<HTMLInputElement | null>(null);
|
|
|
|
|
|
const bodyTextarea = ref<HTMLTextAreaElement | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const MOBILE_MQ = '(max-width: 860px)';
|
|
|
|
|
|
const isMobile = ref(false);
|
|
|
|
|
|
let mq: MediaQueryList | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
const categoryChips: Array<{ value: NoteType | 'all'; label: string; tone: string }> = [
|
|
|
|
|
|
{ value: 'all', label: 'همه', tone: 'all' },
|
|
|
|
|
|
{ value: 'general', label: 'معمولی', tone: 'general' },
|
|
|
|
|
|
{ value: 'follow_up', label: 'پیگیری', tone: 'follow_up' },
|
|
|
|
|
|
{ value: 'idea', label: 'ایده', tone: 'idea' },
|
|
|
|
|
|
{ value: 'suggestion', label: 'پیشنهاد', tone: 'suggestion' },
|
|
|
|
|
|
{ value: 'meeting', label: 'جلسه', tone: 'meeting' },
|
|
|
|
|
|
{ value: 'personal', label: 'شخصی', tone: 'personal' },
|
|
|
|
|
|
];
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
|
|
|
|
|
const noteTypeOptions: Array<{ value: NoteType; label: string }> = [
|
|
|
|
|
|
{ value: 'general', label: 'معمولی' },
|
2026-09-05 19:13:01 +03:30
|
|
|
|
{ value: 'follow_up', label: 'پیگیری' },
|
|
|
|
|
|
{ value: 'idea', label: 'ایده' },
|
|
|
|
|
|
{ value: 'suggestion', label: 'پیشنهاد' },
|
2026-08-29 21:06:30 +03:30
|
|
|
|
{ value: 'meeting', label: 'جلسه' },
|
2026-09-05 19:13:01 +03:30
|
|
|
|
{ value: 'personal', label: 'شخصی' },
|
|
|
|
|
|
{ value: 'call_result', label: 'نتیجه تماس' },
|
2026-08-29 21:06:30 +03:30
|
|
|
|
{ value: 'important', label: 'نکته مهم' },
|
|
|
|
|
|
{ value: 'warning', label: 'هشدار' },
|
|
|
|
|
|
{ value: 'next_action', label: 'کار بعدی' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const colorSwatches: Array<{ value: ColorKey; label: string }> = [
|
|
|
|
|
|
{ value: 'sand', label: 'شن' },
|
|
|
|
|
|
{ value: 'rose', label: 'گل' },
|
|
|
|
|
|
{ value: 'sky', label: 'آسمان' },
|
|
|
|
|
|
{ value: 'mint', label: 'نعناع' },
|
|
|
|
|
|
{ value: 'lavender', label: 'یاسی' },
|
|
|
|
|
|
{ value: 'default', label: 'سفید' },
|
|
|
|
|
|
];
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const typeLabelMap: Record<string, string> = {
|
|
|
|
|
|
general: 'معمولی',
|
|
|
|
|
|
follow_up: 'پیگیری',
|
|
|
|
|
|
idea: 'ایده',
|
|
|
|
|
|
suggestion: 'پیشنهاد',
|
|
|
|
|
|
meeting: 'جلسه',
|
|
|
|
|
|
personal: 'شخصی',
|
|
|
|
|
|
call_result: 'نتیجه تماس',
|
|
|
|
|
|
important: 'نکته مهم',
|
|
|
|
|
|
warning: 'هشدار',
|
|
|
|
|
|
next_action: 'کار بعدی',
|
|
|
|
|
|
};
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const editorTitle = computed(() => (editingId.value ? 'ویرایش' : 'یادداشت جدید'));
|
|
|
|
|
|
const draftColorCss = computed(() => colorToCss(draftColor.value));
|
|
|
|
|
|
const personName = (id: string) =>
|
|
|
|
|
|
people.value.find((p) => p.id === id)?.fullName ?? '';
|
|
|
|
|
|
|
2026-09-05 19:41:39 +03:30
|
|
|
|
/** Hex only — panel is Teleported outside `.notes-page`, so CSS vars would not inherit. */
|
|
|
|
|
|
const NOTE_COLOR_HEX: Record<ColorKey, string> = {
|
|
|
|
|
|
sand: '#fff8e1',
|
|
|
|
|
|
rose: '#fce4ec',
|
|
|
|
|
|
sky: '#e3f2fd',
|
|
|
|
|
|
mint: '#e0f2f1',
|
|
|
|
|
|
lavender: '#f3e5f5',
|
|
|
|
|
|
default: '#ffffff',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
function colorToCss(raw: string | null | undefined): string {
|
2026-09-05 19:41:39 +03:30
|
|
|
|
return NOTE_COLOR_HEX[normalizeColor(raw)];
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeColor(raw: string | null | undefined): ColorKey {
|
|
|
|
|
|
const c = (raw || 'default').toLowerCase();
|
|
|
|
|
|
if (c === 'yellow' || c === 'sand') return 'sand';
|
|
|
|
|
|
if (c === 'pink' || c === 'rose') return 'rose';
|
|
|
|
|
|
if (c === 'blue' || c === 'sky') return 'sky';
|
|
|
|
|
|
if (c === 'teal' || c === 'mint' || c === 'green') return 'mint';
|
|
|
|
|
|
if (c === 'purple' || c === 'lavender') return 'lavender';
|
|
|
|
|
|
if (c === 'white' || c === 'default' || !c) return 'default';
|
|
|
|
|
|
if (c === 'sand' || c === 'rose' || c === 'sky' || c === 'mint' || c === 'lavender') {
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'default';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function typeLabel(t: string) {
|
|
|
|
|
|
return typeLabelMap[t] ?? t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function typeTone(t: string) {
|
|
|
|
|
|
if (t === 'call_result' || t === 'next_action') return 'follow_up';
|
|
|
|
|
|
if (t === 'important') return 'suggestion';
|
|
|
|
|
|
if (t === 'warning') return 'follow_up';
|
|
|
|
|
|
if (
|
|
|
|
|
|
t === 'general' ||
|
|
|
|
|
|
t === 'follow_up' ||
|
|
|
|
|
|
t === 'idea' ||
|
|
|
|
|
|
t === 'suggestion' ||
|
|
|
|
|
|
t === 'meeting' ||
|
|
|
|
|
|
t === 'personal'
|
|
|
|
|
|
) {
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'general';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatNoteTime(iso: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const d = new Date(iso);
|
|
|
|
|
|
if (Number.isNaN(d.getTime())) return iso;
|
|
|
|
|
|
const days = calendarDaysBetween(d, new Date());
|
|
|
|
|
|
if (days <= 0) return `${formatClockFa(d)} امروز`;
|
|
|
|
|
|
if (days === 1) return 'دیروز';
|
|
|
|
|
|
return formatJalaliDay(d);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return iso;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseTags(raw: string) {
|
|
|
|
|
|
return raw
|
|
|
|
|
|
.split(/[,\s#]+/)
|
|
|
|
|
|
.map((t) => t.trim())
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function newChecklistItem(text = ''): NoteChecklistItemDto {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: crypto.randomUUID().slice(0, 12),
|
|
|
|
|
|
text,
|
|
|
|
|
|
done: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function previewBody(body: string) {
|
|
|
|
|
|
const t = body.trim();
|
|
|
|
|
|
if (t.length <= 160) return t;
|
|
|
|
|
|
return `${t.slice(0, 160)}…`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isImageAtt(att: NoteAttachmentDto) {
|
|
|
|
|
|
return att.mimeType.startsWith('image/');
|
|
|
|
|
|
}
|
|
|
|
|
|
function isPdfAtt(att: NoteAttachmentDto) {
|
|
|
|
|
|
return att.mimeType === 'application/pdf' || /\.pdf$/i.test(att.fileName);
|
|
|
|
|
|
}
|
|
|
|
|
|
function isAudioAtt(att: NoteAttachmentDto) {
|
|
|
|
|
|
return att.mimeType.startsWith('audio/');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function firstImage(note: NoteDto) {
|
|
|
|
|
|
return note.attachments.find(isImageAtt) ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
function firstPdf(note: NoteDto) {
|
|
|
|
|
|
return note.attachments.find(isPdfAtt) ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
function firstAudio(note: NoteDto) {
|
|
|
|
|
|
return note.attachments.find(isAudioAtt) ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function blobKey(noteId: string, attId: string) {
|
|
|
|
|
|
return `${noteId}:${attId}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function ensureBlob(noteId: string, att: NoteAttachmentDto) {
|
|
|
|
|
|
const key = blobKey(noteId, att.id);
|
|
|
|
|
|
if (blobCache.value[key]) return blobCache.value[key];
|
|
|
|
|
|
const { data } = await api.get<Blob>(
|
|
|
|
|
|
`/notes/${noteId}/attachments/${att.id}/download`,
|
|
|
|
|
|
{ responseType: 'blob' },
|
|
|
|
|
|
);
|
|
|
|
|
|
const url = URL.createObjectURL(data);
|
|
|
|
|
|
blobCache.value = { ...blobCache.value, [key]: url };
|
|
|
|
|
|
return url;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function prefetchCardMedia(items: NoteDto[]) {
|
|
|
|
|
|
for (const note of items) {
|
|
|
|
|
|
const img = firstImage(note);
|
|
|
|
|
|
const audio = firstAudio(note);
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (img) await ensureBlob(note.id, img);
|
|
|
|
|
|
if (audio) {
|
|
|
|
|
|
const url = await ensureBlob(note.id, audio);
|
|
|
|
|
|
probeAudioDuration(blobKey(note.id, audio.id), url);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* ignore missing blobs */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function probeAudioDuration(key: string, url: string) {
|
|
|
|
|
|
const el = new Audio(url);
|
|
|
|
|
|
el.addEventListener('loadedmetadata', () => {
|
|
|
|
|
|
const sec = Math.round(el.duration || 0);
|
|
|
|
|
|
if (!Number.isFinite(sec) || sec <= 0) return;
|
|
|
|
|
|
const mm = String(Math.floor(sec / 60)).padStart(2, '0');
|
|
|
|
|
|
const ss = String(sec % 60).padStart(2, '0');
|
|
|
|
|
|
audioDurations.value = {
|
|
|
|
|
|
...audioDurations.value,
|
|
|
|
|
|
[key]: toFaDigits(`${mm}:${ss}`),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function revokeAllBlobs() {
|
|
|
|
|
|
for (const url of Object.values(blobCache.value)) {
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
}
|
|
|
|
|
|
blobCache.value = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshAiBadges(items: NoteDto[]) {
|
|
|
|
|
|
const slice = items.slice(0, 40);
|
|
|
|
|
|
const entries = await Promise.all(
|
|
|
|
|
|
slice.map(async (n) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get<NoteAiSuggestionDto | null>(
|
|
|
|
|
|
`/notes/${n.id}/ai-suggestion`,
|
|
|
|
|
|
);
|
|
|
|
|
|
return [n.id, data?.status === 'pending'] as const;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return [n.id, false] as const;
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
const map: Record<string, boolean> = { ...pendingAi.value };
|
|
|
|
|
|
for (const [id, pending] of entries) map[id] = pending;
|
|
|
|
|
|
pendingAi.value = map;
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [notesRes, peopleRes, boardRes] = await Promise.all([
|
|
|
|
|
|
api.get<PaginatedResponse<NoteDto>>('/notes', {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
q: q.value || undefined,
|
|
|
|
|
|
noteType: typeFilter.value === 'all' ? undefined : typeFilter.value,
|
2026-09-05 19:13:01 +03:30
|
|
|
|
sort: sortKey.value,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
limit: 100,
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
api.get<PaginatedResponse<PersonDto>>('/people', { params: { limit: 100 } }),
|
|
|
|
|
|
api.get<PipelineBoardDto>('/pipelines/board'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
notes.value = notesRes.data.items;
|
|
|
|
|
|
people.value = peopleRes.data.items;
|
|
|
|
|
|
deals.value = boardRes.data.stages.flatMap((s) => s.deals);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
void prefetchCardMedia(notes.value);
|
|
|
|
|
|
void refreshAiBadges(notes.value);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (axios.isAxiosError(e) && !e.response) {
|
|
|
|
|
|
error.value = 'اتصال به API برقرار نشد.';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
error.value = 'بارگذاری یادداشتها ناموفق بود.';
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
async function loadActivities() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get<PaginatedResponse<ActivityDto>>('/activities', {
|
|
|
|
|
|
params: { limit: 50 },
|
|
|
|
|
|
});
|
|
|
|
|
|
activities.value = data.items;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
activities.value = [];
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetDraft() {
|
|
|
|
|
|
draftTitle.value = '';
|
2026-09-05 19:13:01 +03:30
|
|
|
|
draftBody.value = '';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
draftType.value = 'general';
|
2026-09-05 19:13:01 +03:30
|
|
|
|
draftColor.value = 'sand';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
draftTags.value = '';
|
2026-09-05 19:13:01 +03:30
|
|
|
|
draftPersonId.value = typeof route.query.personId === 'string' ? route.query.personId : '';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
draftDealId.value = '';
|
2026-09-05 19:13:01 +03:30
|
|
|
|
draftActivityId.value = '';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
draftReminder.value = '';
|
2026-09-05 19:13:01 +03:30
|
|
|
|
draftLocation.value = '';
|
|
|
|
|
|
draftAnalyze.value = true;
|
2026-09-05 19:41:39 +03:30
|
|
|
|
draftPinned.value = false;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
draftChecklist.value = [];
|
2026-08-29 21:06:30 +03:30
|
|
|
|
draftFiles.value = [];
|
2026-09-05 19:13:01 +03:30
|
|
|
|
existingAttachments.value = [];
|
|
|
|
|
|
showChecklistEditor.value = false;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
editingId.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
function openEditor(mode: OpenMode = 'default', note?: NoteDto) {
|
|
|
|
|
|
menuOpenId.value = null;
|
|
|
|
|
|
if (note) {
|
|
|
|
|
|
editingId.value = note.id;
|
|
|
|
|
|
draftTitle.value = note.title ?? '';
|
|
|
|
|
|
draftBody.value = note.body;
|
|
|
|
|
|
draftType.value = (note.noteType as NoteType) || 'general';
|
|
|
|
|
|
draftColor.value = normalizeColor(note.color);
|
|
|
|
|
|
draftTags.value = note.tags.join(' ');
|
|
|
|
|
|
draftPersonId.value = note.person?.id ?? '';
|
|
|
|
|
|
draftDealId.value = note.deal?.id ?? '';
|
|
|
|
|
|
draftActivityId.value = note.activity?.id ?? '';
|
|
|
|
|
|
draftReminder.value = note.reminderAt ? note.reminderAt.slice(0, 16) : '';
|
|
|
|
|
|
draftLocation.value = note.locationText ?? '';
|
|
|
|
|
|
draftAnalyze.value = note.analyzeEnabled !== false;
|
2026-09-05 19:41:39 +03:30
|
|
|
|
draftPinned.value = note.pinned;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
draftChecklist.value = [...(note.checklist ?? [])];
|
|
|
|
|
|
draftFiles.value = [];
|
|
|
|
|
|
existingAttachments.value = [...note.attachments];
|
|
|
|
|
|
showChecklistEditor.value = (note.checklist?.length ?? 0) > 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
resetDraft();
|
|
|
|
|
|
if (mode === 'checklist') {
|
|
|
|
|
|
showChecklistEditor.value = true;
|
|
|
|
|
|
draftChecklist.value = [newChecklistItem()];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
editorOpen.value = true;
|
|
|
|
|
|
moreSheetOpen.value = false;
|
|
|
|
|
|
|
|
|
|
|
|
void nextTick(() => {
|
|
|
|
|
|
bodyTextarea.value?.focus();
|
|
|
|
|
|
if (mode === 'image') imageInput.value?.click();
|
|
|
|
|
|
if (mode === 'attach') fileInput.value?.click();
|
|
|
|
|
|
if (mode === 'audio') audioInput.value?.click();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeEditor() {
|
|
|
|
|
|
editorOpen.value = false;
|
|
|
|
|
|
moreSheetOpen.value = false;
|
|
|
|
|
|
resetDraft();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onFilesSelected(event: Event, kind: 'image' | 'file' | 'audio') {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const input = event.target as HTMLInputElement;
|
|
|
|
|
|
const picked = Array.from(input.files ?? []);
|
|
|
|
|
|
input.value = '';
|
2026-09-05 19:13:01 +03:30
|
|
|
|
if (!picked.length) return;
|
|
|
|
|
|
const filtered =
|
|
|
|
|
|
kind === 'image'
|
|
|
|
|
|
? picked.filter((f) => f.type.startsWith('image/'))
|
|
|
|
|
|
: kind === 'audio'
|
|
|
|
|
|
? picked.filter((f) => f.type.startsWith('audio/') || !f.type)
|
|
|
|
|
|
: picked;
|
|
|
|
|
|
draftFiles.value = [...draftFiles.value, ...filtered].slice(0, 8);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeDraftFile(index: number) {
|
|
|
|
|
|
draftFiles.value = draftFiles.value.filter((_, i) => i !== index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
async function removeExistingAttachment(attachmentId: string) {
|
|
|
|
|
|
if (!editingId.value) return;
|
|
|
|
|
|
if (!confirm('این پیوست حذف شود؟')) return;
|
|
|
|
|
|
await api.delete(`/notes/${editingId.value}/attachments/${attachmentId}`);
|
|
|
|
|
|
existingAttachments.value = existingAttachments.value.filter((a) => a.id !== attachmentId);
|
|
|
|
|
|
await load();
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadFiles(noteId: string, files: File[]) {
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
|
const form = new FormData();
|
|
|
|
|
|
form.append('file', file);
|
|
|
|
|
|
await api.post(`/notes/${noteId}/attachments`, form);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
function addChecklistRow() {
|
|
|
|
|
|
draftChecklist.value = [...draftChecklist.value, newChecklistItem()];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeChecklistRow(id: string) {
|
|
|
|
|
|
draftChecklist.value = draftChecklist.value.filter((c) => c.id !== id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleDraftCheck(id: string) {
|
|
|
|
|
|
draftChecklist.value = draftChecklist.value.map((c) =>
|
|
|
|
|
|
c.id === id ? { ...c, done: !c.done } : c,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
async function toggleCardChecklist(note: NoteDto, itemId: string) {
|
|
|
|
|
|
const checklist = (note.checklist ?? []).map((c) =>
|
|
|
|
|
|
c.id === itemId ? { ...c, done: !c.done } : c,
|
|
|
|
|
|
);
|
|
|
|
|
|
note.checklist = checklist;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
try {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
await api.patch(`/notes/${note.id}`, { checklist });
|
|
|
|
|
|
} catch {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
await load();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveNote() {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const body = draftBody.value.trim();
|
|
|
|
|
|
const checklist = draftChecklist.value.filter((c) => c.text.trim());
|
|
|
|
|
|
if (!body && !checklist.length) {
|
|
|
|
|
|
error.value = 'متن یادداشت یا چکلیست لازم است.';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
saving.value = true;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
title: draftTitle.value.trim() || null,
|
|
|
|
|
|
body: body || (checklist.length ? 'چکلیست' : ''),
|
2026-08-29 21:06:30 +03:30
|
|
|
|
noteType: draftType.value,
|
2026-09-05 19:13:01 +03:30
|
|
|
|
color: draftColor.value === 'default' ? 'default' : draftColor.value,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
tags: parseTags(draftTags.value),
|
|
|
|
|
|
personId: draftPersonId.value || null,
|
|
|
|
|
|
dealId: draftDealId.value || null,
|
2026-09-05 19:13:01 +03:30
|
|
|
|
activityId: draftActivityId.value || null,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
reminderAt: draftReminder.value
|
|
|
|
|
|
? new Date(draftReminder.value).toISOString()
|
|
|
|
|
|
: null,
|
2026-09-05 19:13:01 +03:30
|
|
|
|
locationText: draftLocation.value.trim() || null,
|
|
|
|
|
|
analyzeEnabled: draftAnalyze.value,
|
2026-09-05 19:41:39 +03:30
|
|
|
|
pinned: draftPinned.value,
|
2026-09-05 19:13:01 +03:30
|
|
|
|
checklist,
|
2026-08-29 21:06:30 +03:30
|
|
|
|
};
|
|
|
|
|
|
const pending = [...draftFiles.value];
|
|
|
|
|
|
let noteId = editingId.value;
|
|
|
|
|
|
if (editingId.value) {
|
|
|
|
|
|
await api.patch(`/notes/${editingId.value}`, payload);
|
|
|
|
|
|
} else {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
const { data } = await api.post<NoteDto>('/notes', {
|
|
|
|
|
|
...payload,
|
|
|
|
|
|
title: draftTitle.value.trim() || undefined,
|
|
|
|
|
|
});
|
2026-08-29 21:06:30 +03:30
|
|
|
|
noteId = data.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (noteId && pending.length) {
|
|
|
|
|
|
await uploadFiles(noteId, pending);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
draftFiles.value = [];
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
if (noteId && draftAnalyze.value && body.length >= 8) {
|
|
|
|
|
|
void api.post(`/notes/${noteId}/analyze`).catch(() => undefined);
|
2026-09-01 14:29:34 +03:30
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
closeEditor();
|
2026-08-29 21:06:30 +03:30
|
|
|
|
await load();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (axios.isAxiosError(e)) {
|
|
|
|
|
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
|
|
|
|
|
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ذخیره یادداشت ناموفق بود');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
error.value = 'ذخیره یادداشت ناموفق بود';
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function togglePin(id: string) {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
menuOpenId.value = null;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
await api.patch(`/notes/${id}/pin`);
|
|
|
|
|
|
await load();
|
2026-09-05 19:41:39 +03:30
|
|
|
|
if (editingId.value === id) {
|
|
|
|
|
|
const n = notes.value.find((x) => x.id === id);
|
|
|
|
|
|
if (n) draftPinned.value = n.pinned;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function onPinClick() {
|
|
|
|
|
|
const next = !draftPinned.value;
|
|
|
|
|
|
draftPinned.value = next;
|
|
|
|
|
|
if (!editingId.value) return;
|
|
|
|
|
|
const note = notes.value.find((n) => n.id === editingId.value);
|
|
|
|
|
|
if (note && note.pinned === next) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.patch(`/notes/${editingId.value}/pin`);
|
|
|
|
|
|
if (note) note.pinned = next;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
draftPinned.value = !next;
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function removeNote(id: string) {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
menuOpenId.value = null;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
if (!confirm('این یادداشت حذف شود؟')) return;
|
|
|
|
|
|
await api.delete(`/notes/${id}`);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
if (editingId.value === id) closeEditor();
|
2026-08-29 21:06:30 +03:30
|
|
|
|
await load();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
function promptLocation() {
|
|
|
|
|
|
const next = window.prompt('موقعیت مکانی', draftLocation.value || '');
|
|
|
|
|
|
if (next == null) return;
|
|
|
|
|
|
draftLocation.value = next.trim();
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
function openMoreSheet() {
|
|
|
|
|
|
moreSheetOpen.value = true;
|
|
|
|
|
|
if (!activities.value.length) void loadActivities();
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
function syncMobileFlag() {
|
|
|
|
|
|
isMobile.value = mq?.matches ?? false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onDocClick(ev: MouseEvent) {
|
|
|
|
|
|
const t = ev.target as HTMLElement | null;
|
|
|
|
|
|
if (!t?.closest?.('.card-menu')) menuOpenId.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(typeFilter, () => {
|
|
|
|
|
|
void load();
|
|
|
|
|
|
});
|
|
|
|
|
|
watch(sortKey, () => {
|
|
|
|
|
|
void load();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
mq = window.matchMedia(MOBILE_MQ);
|
|
|
|
|
|
syncMobileFlag();
|
|
|
|
|
|
mq.addEventListener('change', syncMobileFlag);
|
|
|
|
|
|
document.addEventListener('click', onDocClick);
|
|
|
|
|
|
if (typeof route.query.q === 'string' && route.query.q) q.value = route.query.q;
|
|
|
|
|
|
if (typeof route.query.personId === 'string' && route.query.personId) {
|
|
|
|
|
|
draftPersonId.value = route.query.personId;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
void load();
|
|
|
|
|
|
void loadActivities();
|
|
|
|
|
|
});
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
mq?.removeEventListener('change', syncMobileFlag);
|
|
|
|
|
|
document.removeEventListener('click', onDocClick);
|
|
|
|
|
|
revokeAllBlobs();
|
|
|
|
|
|
});
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<section class="notes-page">
|
2026-08-29 21:06:30 +03:30
|
|
|
|
<header class="page-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1>یادداشتها</h1>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<p class="lead">فکر کنید، یادداشت کنید، هیچ چیز را فراموش نکنید.</p>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<p v-if="error" class="banner-error">{{ error }}</p>
|
2026-09-03 14:20:55 +03:30
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<!-- Desktop toolbar -->
|
|
|
|
|
|
<div class="toolbar desktop-only">
|
|
|
|
|
|
<div class="chips">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="chip in categoryChips"
|
|
|
|
|
|
:key="chip.value"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="chip"
|
|
|
|
|
|
:class="[`tone-${chip.tone}`, { active: typeFilter === chip.value }]"
|
|
|
|
|
|
@click="typeFilter = chip.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span v-if="chip.value !== 'all'" class="dot" />
|
|
|
|
|
|
{{ chip.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="toolbar-end">
|
|
|
|
|
|
<div class="view-toggle" role="group" aria-label="نمای لیست">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:class="{ active: viewMode === 'grid' }"
|
|
|
|
|
|
title="شبکه"
|
|
|
|
|
|
@click="viewMode = 'grid'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon name="grid" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:class="{ active: viewMode === 'list' }"
|
|
|
|
|
|
title="لیست"
|
|
|
|
|
|
@click="viewMode = 'list'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon name="list" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<select v-model="sortKey" class="sort-select" aria-label="مرتبسازی">
|
|
|
|
|
|
<option value="updatedAt">جدیدترین</option>
|
|
|
|
|
|
<option value="createdAt">تاریخ ایجاد</option>
|
|
|
|
|
|
<option value="reminderAt">یادآوری</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button type="button" class="icon-btn" title="فیلتر" @click="q = q; load()">
|
|
|
|
|
|
<UiIcon name="filter" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Desktop quick-add -->
|
|
|
|
|
|
<div class="quick-add desktop-only" @click="openEditor()">
|
|
|
|
|
|
<input
|
|
|
|
|
|
class="quick-input"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
readonly
|
|
|
|
|
|
placeholder="یادداشتی بنویسید..."
|
|
|
|
|
|
@focus.prevent="openEditor()"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="quick-actions" @click.stop>
|
|
|
|
|
|
<button type="button" class="icon-btn" title="تصویر" @click="openEditor('image')">
|
|
|
|
|
|
<UiIcon name="layers" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="icon-btn" title="صدا" @click="openEditor('mic')">
|
|
|
|
|
|
<UiIcon name="mic" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="icon-btn" title="پیوست" @click="openEditor('attach')">
|
|
|
|
|
|
<UiIcon name="document" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Mobile quick bar -->
|
|
|
|
|
|
<div class="mobile-quick mobile-only">
|
|
|
|
|
|
<button type="button" class="plus-teal" @click="openEditor()">
|
|
|
|
|
|
<UiIcon name="plus" :size="20" />
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</button>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<input
|
|
|
|
|
|
class="quick-input"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
readonly
|
|
|
|
|
|
placeholder="یادداشتی بنویسید..."
|
|
|
|
|
|
@focus.prevent="openEditor()"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button type="button" class="icon-btn" @click="openEditor('image')">
|
|
|
|
|
|
<UiIcon name="layers" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="icon-btn" @click="openEditor('mic')">
|
|
|
|
|
|
<UiIcon name="mic" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="icon-btn" @click="openEditor('attach')">
|
|
|
|
|
|
<UiIcon name="document" :size="18" />
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<!-- Mobile chips -->
|
|
|
|
|
|
<div class="chips chips-scroll mobile-only">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="chip in categoryChips"
|
|
|
|
|
|
:key="`m-${chip.value}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="chip"
|
|
|
|
|
|
:class="[`tone-${chip.tone}`, { active: typeFilter === chip.value }]"
|
|
|
|
|
|
@click="typeFilter = chip.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span v-if="chip.value !== 'all'" class="dot" />
|
|
|
|
|
|
{{ chip.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-if="loading" class="state-msg">در حال بارگذاری…</p>
|
|
|
|
|
|
<p v-else-if="!notes.length" class="state-msg">هنوز یادداشتی نیست — یکی اضافه کنید.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="notes-board"
|
|
|
|
|
|
:class="viewMode === 'list' ? 'is-list' : 'is-grid'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="note in notes"
|
|
|
|
|
|
:key="note.id"
|
|
|
|
|
|
class="note-card"
|
|
|
|
|
|
:style="{ background: colorToCss(note.color) }"
|
|
|
|
|
|
@click="openEditor('default', note)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="card-top">
|
|
|
|
|
|
<span v-if="note.pinned" class="pin" title="سنجاقشده" aria-hidden="true">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M14.5 3.5 16 5l-1.2 1.2 3.7 3.7 1.3-1.3 1.4 1.4-7.8 7.8-1.4-1.4 1.3-1.3-3.7-3.7L8 13.5 6.5 12l8-8.5ZM7 18l-3 3"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div class="card-menu" @click.stop>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="icon-btn menu-btn"
|
|
|
|
|
|
@click="menuOpenId = menuOpenId === note.id ? null : note.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon name="dots-v" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div v-if="menuOpenId === note.id" class="menu-pop">
|
|
|
|
|
|
<button type="button" @click="openEditor('default', note)">ویرایش</button>
|
|
|
|
|
|
<button type="button" @click="togglePin(note.id)">
|
|
|
|
|
|
{{ note.pinned ? 'برداشتن سنجاق' : 'سنجاق کردن' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="danger" @click="removeNote(note.id)">حذف</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<h3 v-if="note.title" class="card-title">{{ note.title }}</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<ul v-if="note.checklist?.length" class="card-check" @click.stop>
|
|
|
|
|
|
<li v-for="item in note.checklist" :key="item.id">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
:checked="item.done"
|
|
|
|
|
|
@change="toggleCardChecklist(note, item.id)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span :class="{ done: item.done }">{{ item.text }}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<p v-else-if="note.body" class="card-body">{{ previewBody(note.body) }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="firstImage(note)" class="card-thumb">
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="blobCache[blobKey(note.id, firstImage(note)!.id)]"
|
|
|
|
|
|
:src="blobCache[blobKey(note.id, firstImage(note)!.id)]"
|
|
|
|
|
|
:alt="firstImage(note)!.fileName"
|
2026-08-29 21:06:30 +03:30
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
|
|
|
|
|
|
<div v-if="firstPdf(note)" class="att-chip pdf">
|
|
|
|
|
|
<UiIcon name="document" :size="14" />
|
|
|
|
|
|
<span>{{ firstPdf(note)!.fileName }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="firstAudio(note) && blobCache[blobKey(note.id, firstAudio(note)!.id)]"
|
|
|
|
|
|
class="audio-row"
|
|
|
|
|
|
@click.stop
|
|
|
|
|
|
>
|
|
|
|
|
|
<audio
|
|
|
|
|
|
controls
|
|
|
|
|
|
preload="metadata"
|
|
|
|
|
|
:src="blobCache[blobKey(note.id, firstAudio(note)!.id)]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="audioDurations[blobKey(note.id, firstAudio(note)!.id)]"
|
|
|
|
|
|
class="dur"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ audioDurations[blobKey(note.id, firstAudio(note)!.id)] }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card-meta">
|
|
|
|
|
|
<span class="type-badge" :class="`tone-${typeTone(String(note.noteType))}`">
|
|
|
|
|
|
{{ typeLabel(String(note.noteType)) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="pendingAi[note.id]" class="ai-badge" title="پیشنهاد AI">
|
|
|
|
|
|
<UiIcon name="sparkle" :size="12" />
|
|
|
|
|
|
AI
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="card-foot">
|
|
|
|
|
|
<time>{{ formatNoteTime(note.updatedAt) }}</time>
|
|
|
|
|
|
<span v-if="note.person" class="person">{{ note.person.fullName }}</span>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-09-05 19:59:47 +03:30
|
|
|
|
class="note-card add-card"
|
2026-09-05 19:13:01 +03:30
|
|
|
|
@click="openEditor()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon name="plus" :size="22" />
|
|
|
|
|
|
<span>یادداشت جدید</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Mobile FAB -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="!editorOpen"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="fab mobile-only"
|
|
|
|
|
|
aria-label="یادداشت جدید"
|
|
|
|
|
|
@click="openEditor()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon name="plus" :size="24" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Hidden file inputs -->
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref="imageInput"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept="image/*"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
hidden
|
|
|
|
|
|
@change="onFilesSelected($event, 'image')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref="fileInput"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
hidden
|
|
|
|
|
|
@change="onFilesSelected($event, 'file')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref="audioInput"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept="audio/*"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
hidden
|
|
|
|
|
|
@change="onFilesSelected($event, 'audio')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<!-- Unified create/edit modal -->
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<Teleport to="body">
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<div v-if="editorOpen" class="panel-root">
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<div class="panel-backdrop" @click="closeEditor" />
|
2026-09-05 19:41:39 +03:30
|
|
|
|
<aside
|
|
|
|
|
|
class="editor-panel"
|
|
|
|
|
|
:style="{
|
|
|
|
|
|
'--canvas': draftColorCss,
|
|
|
|
|
|
background: draftColorCss,
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<header class="panel-head">
|
|
|
|
|
|
<h2>{{ editorTitle }}</h2>
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<div class="panel-head-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="btn-save mobile-only-flex"
|
|
|
|
|
|
:disabled="saving"
|
|
|
|
|
|
@click="saveNote"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ saving ? '…' : 'ذخیره' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="icon-btn" @click="closeEditor">
|
|
|
|
|
|
<UiIcon name="x" :size="20" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel-scroll">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="draftTitle"
|
|
|
|
|
|
class="title-input"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="عنوان (اختیاری)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
ref="bodyTextarea"
|
|
|
|
|
|
v-model="draftBody"
|
|
|
|
|
|
class="body-canvas"
|
|
|
|
|
|
rows="8"
|
|
|
|
|
|
placeholder="یادداشت خود را بنویسید…"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-09-05 19:41:39 +03:30
|
|
|
|
<div class="palette-row">
|
|
|
|
|
|
<div class="swatches" role="listbox" aria-label="رنگ یادداشت">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="sw in colorSwatches"
|
|
|
|
|
|
:key="sw.value"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="swatch"
|
2026-09-05 19:59:47 +03:30
|
|
|
|
:class="[`tone-${sw.value}`, { selected: draftColor === sw.value }]"
|
2026-09-05 19:41:39 +03:30
|
|
|
|
:style="{ backgroundColor: NOTE_COLOR_HEX[sw.value] }"
|
|
|
|
|
|
:title="sw.label"
|
|
|
|
|
|
:aria-label="sw.label"
|
|
|
|
|
|
:aria-selected="draftColor === sw.value"
|
|
|
|
|
|
@click="draftColor = sw.value"
|
|
|
|
|
|
>
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<UiIcon v-if="draftColor === sw.value" name="check-plain" :size="14" />
|
2026-09-05 19:41:39 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-09-05 19:41:39 +03:30
|
|
|
|
class="media-btn pin-btn"
|
|
|
|
|
|
:class="{ on: draftPinned }"
|
|
|
|
|
|
title="سنجاق"
|
|
|
|
|
|
@click="onPinClick"
|
2026-09-05 19:13:01 +03:30
|
|
|
|
>
|
2026-09-05 19:41:39 +03:30
|
|
|
|
سنجاق
|
2026-09-05 19:13:01 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<div class="media-row desktop-only-flex">
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<button type="button" class="media-btn" @click="imageInput?.click()">
|
|
|
|
|
|
<UiIcon name="layers" :size="18" /> تصویر
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="media-btn" @click="fileInput?.click()">
|
|
|
|
|
|
<UiIcon name="document" :size="18" /> فایل
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="media-btn" @click="audioInput?.click()">
|
|
|
|
|
|
<UiIcon name="mic" :size="18" /> صدا
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="media-btn"
|
|
|
|
|
|
:class="{ on: showChecklistEditor }"
|
|
|
|
|
|
@click="
|
|
|
|
|
|
showChecklistEditor = !showChecklistEditor;
|
|
|
|
|
|
if (showChecklistEditor && !draftChecklist.length)
|
|
|
|
|
|
draftChecklist = [newChecklistItem()];
|
|
|
|
|
|
"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon name="check" :size="18" /> چکلیست
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<VoiceToTextButton
|
|
|
|
|
|
variant="toolbar"
|
|
|
|
|
|
hide-error
|
|
|
|
|
|
@transcript="(t) => (draftBody = appendSpokenText(draftBody, t))"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="showChecklistEditor" class="checklist-ed">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(item, idx) in draftChecklist"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
class="check-row"
|
|
|
|
|
|
>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
:checked="item.done"
|
|
|
|
|
|
@change="toggleDraftCheck(item.id)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="draftChecklist[idx]!.text"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="مورد چکلیست"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button type="button" class="icon-btn" @click="removeChecklistRow(item.id)">
|
|
|
|
|
|
<UiIcon name="x" :size="16" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button type="button" class="linkish" @click="addChecklistRow">+ مورد جدید</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="draftFiles.length || existingAttachments.length" class="pending-files">
|
|
|
|
|
|
<div v-for="(f, i) in draftFiles" :key="`df-${i}`" class="file-chip">
|
|
|
|
|
|
<span>{{ f.name }}</span>
|
|
|
|
|
|
<button type="button" @click="removeDraftFile(i)">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="att in existingAttachments"
|
|
|
|
|
|
:key="att.id"
|
|
|
|
|
|
class="file-chip"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>{{ att.fileName }}</span>
|
|
|
|
|
|
<button type="button" @click="removeExistingAttachment(att.id)">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<div class="desktop-fields">
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<label class="field">
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<span>نوع یادداشت</span>
|
|
|
|
|
|
<select v-model="draftType">
|
|
|
|
|
|
<option
|
|
|
|
|
|
v-for="opt in noteTypeOptions"
|
|
|
|
|
|
:key="opt.value"
|
|
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ opt.label }}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<div class="field-row">
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>مرتبط با — مشتری</span>
|
|
|
|
|
|
<select v-model="draftPersonId">
|
|
|
|
|
|
<option value="">—</option>
|
|
|
|
|
|
<option v-for="p in people" :key="p.id" :value="p.id">
|
|
|
|
|
|
{{ p.fullName }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>معامله</span>
|
|
|
|
|
|
<select v-model="draftDealId">
|
|
|
|
|
|
<option value="">—</option>
|
|
|
|
|
|
<option v-for="d in deals" :key="d.id" :value="d.id">
|
|
|
|
|
|
{{ d.title }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<label class="field">
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<span>یادآوری</span>
|
|
|
|
|
|
<JalaliDateInput v-model="draftReminder" mode="datetime" icon="bell" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>برچسبها</span>
|
|
|
|
|
|
<input v-model="draftTags" type="text" placeholder="مثلاً قیمت جلسه" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>موقعیت</span>
|
|
|
|
|
|
<input v-model="draftLocation" type="text" placeholder="اختیاری" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="ai-toggle">
|
|
|
|
|
|
<input v-model="draftAnalyze" type="checkbox" />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>تحلیل با هوش مصنوعی</strong>
|
|
|
|
|
|
<p>پس از ذخیره، پیشنهاد کار بعدی و حافظه مشتری استخراج میشود.</p>
|
|
|
|
|
|
</div>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<div class="chip-summary mobile-only-flex">
|
|
|
|
|
|
<span v-for="(f, i) in draftFiles" :key="`mf-${i}`" class="tag-chip">{{ f.name }}</span>
|
|
|
|
|
|
<span v-for="t in parseTags(draftTags)" :key="t" class="tag-chip">#{{ t }}</span>
|
|
|
|
|
|
<span v-if="draftPersonId" class="tag-chip">{{ personName(draftPersonId) }}</span>
|
|
|
|
|
|
<span v-if="draftDealId" class="tag-chip">معامله</span>
|
|
|
|
|
|
<span v-if="draftLocation" class="tag-chip">{{ draftLocation }}</span>
|
|
|
|
|
|
</div>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
|
|
|
|
|
|
<NoteAiSuggestionPanel
|
|
|
|
|
|
v-if="editingId"
|
|
|
|
|
|
:note-id="editingId"
|
|
|
|
|
|
:has-person="Boolean(draftPersonId)"
|
|
|
|
|
|
@applied="load"
|
|
|
|
|
|
@dismissed="load"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<footer class="panel-foot desktop-only-flex">
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="btn-primary"
|
|
|
|
|
|
:disabled="saving"
|
|
|
|
|
|
@click="saveNote"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره یادداشت' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="btn-ghost" @click="closeEditor">لغو</button>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<nav class="mob-toolbar mobile-only-flex">
|
2026-09-05 19:13:01 +03:30
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-09-05 19:59:47 +03:30
|
|
|
|
@click="
|
|
|
|
|
|
showChecklistEditor = !showChecklistEditor;
|
|
|
|
|
|
if (showChecklistEditor && !draftChecklist.length)
|
|
|
|
|
|
draftChecklist = [newChecklistItem()];
|
|
|
|
|
|
"
|
2026-09-05 19:13:01 +03:30
|
|
|
|
>
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<UiIcon name="check" :size="20" />
|
|
|
|
|
|
<span>چکلیست</span>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
</button>
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<div class="voice-slot">
|
|
|
|
|
|
<VoiceToTextButton
|
|
|
|
|
|
variant="toolbar"
|
|
|
|
|
|
hide-error
|
|
|
|
|
|
@transcript="(t) => (draftBody = appendSpokenText(draftBody, t))"
|
2026-09-05 19:13:01 +03:30
|
|
|
|
/>
|
2026-09-05 19:59:47 +03:30
|
|
|
|
<button type="button" class="tiny" @click="audioInput?.click()">فایل صدا</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button type="button" @click="imageInput?.click()">
|
|
|
|
|
|
<UiIcon name="layers" :size="20" />
|
|
|
|
|
|
<span>عکس</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" @click="fileInput?.click()">
|
|
|
|
|
|
<UiIcon name="document" :size="20" />
|
|
|
|
|
|
<span>فایل</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" @click="promptLocation">
|
|
|
|
|
|
<UiIcon name="flag" :size="20" />
|
|
|
|
|
|
<span>موقعیت</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" @click="openMoreSheet">
|
|
|
|
|
|
<UiIcon name="dots" :size="20" />
|
|
|
|
|
|
<span>بیشتر</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="moreSheetOpen" class="sheet-root">
|
|
|
|
|
|
<div class="sheet-backdrop" @click="moreSheetOpen = false" />
|
|
|
|
|
|
<div class="sheet">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<h3>جزئیات بیشتر</h3>
|
|
|
|
|
|
<button type="button" class="icon-btn" @click="moreSheetOpen = false">
|
|
|
|
|
|
<UiIcon name="x" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>انتخاب مشتری</span>
|
|
|
|
|
|
<select v-model="draftPersonId">
|
|
|
|
|
|
<option value="">—</option>
|
|
|
|
|
|
<option v-for="p in people" :key="p.id" :value="p.id">{{ p.fullName }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>معامله</span>
|
|
|
|
|
|
<select v-model="draftDealId">
|
|
|
|
|
|
<option value="">—</option>
|
|
|
|
|
|
<option v-for="d in deals" :key="d.id" :value="d.id">{{ d.title }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>فعالیت</span>
|
|
|
|
|
|
<select v-model="draftActivityId">
|
|
|
|
|
|
<option value="">—</option>
|
|
|
|
|
|
<option v-for="a in activities" :key="a.id" :value="a.id">{{ a.title }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>نوع یادداشت</span>
|
|
|
|
|
|
<select v-model="draftType">
|
|
|
|
|
|
<option
|
|
|
|
|
|
v-for="opt in noteTypeOptions"
|
|
|
|
|
|
:key="opt.value"
|
|
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ opt.label }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>یادآوری</span>
|
|
|
|
|
|
<JalaliDateInput v-model="draftReminder" mode="datetime" icon="bell" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>برچسبها</span>
|
|
|
|
|
|
<input v-model="draftTags" type="text" placeholder="برچسبها" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="ai-toggle">
|
|
|
|
|
|
<input v-model="draftAnalyze" type="checkbox" />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>تحلیل با هوش مصنوعی</strong>
|
|
|
|
|
|
<p>پس از ذخیره اجرا میشود.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<button type="button" class="btn-primary sheet-done" @click="moreSheetOpen = false">
|
|
|
|
|
|
تأیید
|
2026-09-05 19:13:01 +03:30
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-05 19:59:47 +03:30
|
|
|
|
</aside>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</div>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
</Teleport>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.notes-page {
|
|
|
|
|
|
--note-sand: #fff8e1;
|
|
|
|
|
|
--note-rose: #fce4ec;
|
|
|
|
|
|
--note-sky: #e3f2fd;
|
|
|
|
|
|
--note-mint: #e0f2f1;
|
|
|
|
|
|
--note-lavender: #f3e5f5;
|
|
|
|
|
|
--note-default: #ffffff;
|
|
|
|
|
|
--teal: #0d9488;
|
|
|
|
|
|
--teal-soft: #ccfbf1;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
--radius: 10px;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
--shadow: 0 4px 18px rgba(15, 23, 42, 0.07);
|
2026-09-12 01:33:07 +03:30
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
2026-09-03 14:20:55 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.page-head h1 {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
margin: 0;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
font-size: 1.55rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
font-weight: 600;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
color: #0f172a;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
2026-09-05 01:45:53 +03:30
|
|
|
|
.lead {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
margin: 0.35rem 0 0;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.95rem;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.banner-error {
|
|
|
|
|
|
margin: 0.75rem 0 0;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
padding: 0.65rem 0.85rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
color: #b91c1c;
|
|
|
|
|
|
font-size: 0.9rem;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.toolbar {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
align-items: center;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
justify-content: space-between;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
margin-top: 1.1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar-end {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.45rem;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.chips {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chips-scroll {
|
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
margin: 0.85rem -0.25rem 0;
|
|
|
|
|
|
padding: 0 0.25rem 0.35rem;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
padding: 0.35rem 0.75rem;
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip .dot {
|
|
|
|
|
|
width: 7px;
|
|
|
|
|
|
height: 7px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: currentColor;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-all.active {
|
|
|
|
|
|
background: var(--teal);
|
|
|
|
|
|
border-color: var(--teal);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-general {
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-general.active {
|
|
|
|
|
|
background: #ecfdf5;
|
|
|
|
|
|
border-color: #6ee7b7;
|
|
|
|
|
|
color: #047857;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-follow_up {
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-follow_up.active {
|
|
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
border-color: #fca5a5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-idea {
|
|
|
|
|
|
color: #db2777;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-idea.active {
|
|
|
|
|
|
background: #fdf2f8;
|
|
|
|
|
|
border-color: #f9a8d4;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-suggestion {
|
|
|
|
|
|
color: #d97706;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-suggestion.active {
|
|
|
|
|
|
background: #fffbeb;
|
|
|
|
|
|
border-color: #fcd34d;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-meeting {
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-meeting.active {
|
|
|
|
|
|
background: #eff6ff;
|
|
|
|
|
|
border-color: #93c5fd;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-personal {
|
|
|
|
|
|
color: #7c3aed;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip.tone-personal.active {
|
|
|
|
|
|
background: #f5f3ff;
|
|
|
|
|
|
border-color: #c4b5fd;
|
2026-09-05 01:45:53 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.view-toggle {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
overflow: hidden;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.view-toggle button {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
border: 0;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
background: transparent;
|
|
|
|
|
|
padding: 0.35rem 0.45rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.view-toggle button.active {
|
|
|
|
|
|
background: var(--teal-soft);
|
|
|
|
|
|
color: var(--teal);
|
|
|
|
|
|
}
|
|
|
|
|
|
.sort-select {
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.35rem 0.55rem;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
font-size: 0.85rem;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.icon-btn {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #64748b;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.35rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.icon-btn:hover {
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-add {
|
|
|
|
|
|
margin-top: 0.95rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
|
padding: 0.45rem 0.55rem 0.45rem 0.85rem;
|
|
|
|
|
|
cursor: text;
|
|
|
|
|
|
}
|
|
|
|
|
|
.quick-input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.quick-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.15rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mobile-quick {
|
|
|
|
|
|
margin-top: 0.9rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
|
padding: 0.4rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.plus-teal {
|
|
|
|
|
|
width: 38px;
|
|
|
|
|
|
height: 38px;
|
|
|
|
|
|
border: 0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
background: var(--teal);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.state-msg {
|
|
|
|
|
|
margin: 1.5rem 0;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notes-board.is-grid {
|
|
|
|
|
|
margin-top: 1.1rem;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
|
|
|
|
gap: 0.85rem;
|
|
|
|
|
|
align-items: stretch;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.notes-board.is-list {
|
|
|
|
|
|
margin-top: 1.1rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.notes-board.is-list .note-card {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
aspect-ratio: auto;
|
|
|
|
|
|
min-height: 120px;
|
|
|
|
|
|
height: auto;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-card {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
aspect-ratio: 1 / 1;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
margin: 0;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
|
border: 1px solid rgba(15, 23, 42, 0.05);
|
2026-09-05 19:59:47 +03:30
|
|
|
|
padding: 0.75rem 0.8rem 0.65rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
text-align: start;
|
|
|
|
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.note-card:hover {
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 1.5px dashed #cbd5e1;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
font-size: 0.92rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.add-card:hover {
|
|
|
|
|
|
border-color: var(--teal);
|
|
|
|
|
|
color: var(--teal);
|
|
|
|
|
|
background: #f0fdfa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
min-height: 1.25rem;
|
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.pin {
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card-menu {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
margin-inline-start: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.menu-pop {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset-inline-end: 0;
|
|
|
|
|
|
top: 100%;
|
|
|
|
|
|
z-index: 5;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
|
min-width: 140px;
|
|
|
|
|
|
padding: 0.25rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
.menu-pop button {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
text-align: start;
|
|
|
|
|
|
padding: 0.45rem 0.65rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
}
|
|
|
|
|
|
.menu-pop button:hover {
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
}
|
|
|
|
|
|
.menu-pop .danger {
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
|
margin: 0 0 0.35rem;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
font-size: 0.95rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #0f172a;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
overflow: hidden;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.card-body {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
color: #334155;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
line-height: 1.45;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 6;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.card-check {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
list-style: none;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow: hidden;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.card-check label {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.45rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
font-size: 0.88rem;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card-check .done {
|
|
|
|
|
|
text-decoration: line-through;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-thumb {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
margin-top: 0.45rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
overflow: hidden;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
max-height: 72px;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.card-thumb img {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
height: 72px;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.att-chip {
|
|
|
|
|
|
margin-top: 0.55rem;
|
|
|
|
|
|
display: inline-flex;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
align-items: center;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
gap: 0.3rem;
|
|
|
|
|
|
padding: 0.25rem 0.5rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
background: rgba(15, 23, 42, 0.06);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
max-width: 100%;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.att-chip span {
|
2026-09-03 14:20:55 +03:30
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
white-space: nowrap;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.audio-row {
|
|
|
|
|
|
margin-top: 0.55rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-row audio {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-row .dur {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
flex-shrink: 0;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.card-meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-09-03 14:20:55 +03:30
|
|
|
|
gap: 0.35rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
align-items: center;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
margin-top: 0.35rem;
|
|
|
|
|
|
flex-shrink: 0;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.type-badge {
|
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
padding: 0.18rem 0.5rem;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
}
|
|
|
|
|
|
.type-badge.tone-general {
|
|
|
|
|
|
background: #ecfdf5;
|
|
|
|
|
|
color: #047857;
|
|
|
|
|
|
}
|
|
|
|
|
|
.type-badge.tone-follow_up {
|
|
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
}
|
|
|
|
|
|
.type-badge.tone-idea {
|
|
|
|
|
|
background: #fdf2f8;
|
|
|
|
|
|
color: #db2777;
|
|
|
|
|
|
}
|
|
|
|
|
|
.type-badge.tone-suggestion {
|
|
|
|
|
|
background: #fffbeb;
|
|
|
|
|
|
color: #d97706;
|
|
|
|
|
|
}
|
|
|
|
|
|
.type-badge.tone-meeting {
|
|
|
|
|
|
background: #eff6ff;
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
.type-badge.tone-personal {
|
|
|
|
|
|
background: #f5f3ff;
|
|
|
|
|
|
color: #7c3aed;
|
|
|
|
|
|
}
|
|
|
|
|
|
.ai-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.15rem;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--teal);
|
|
|
|
|
|
background: var(--teal-soft);
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
padding: 0.15rem 0.4rem;
|
2026-09-03 14:20:55 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.card-foot {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
margin-top: auto;
|
|
|
|
|
|
padding-top: 0.45rem;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 0.5rem;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
font-size: 0.72rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
color: #94a3b8;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
flex-shrink: 0;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.card-foot .person {
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.fab {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset-inline-end: 1.1rem;
|
|
|
|
|
|
bottom: 1.25rem;
|
|
|
|
|
|
z-index: 40;
|
|
|
|
|
|
width: 54px;
|
|
|
|
|
|
height: 54px;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
border: 0;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--teal);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
box-shadow: 0 10px 28px rgba(13, 148, 136, 0.4);
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
cursor: pointer;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-root {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 80;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
--note-sand: #fff8e1;
|
|
|
|
|
|
--note-rose: #fce4ec;
|
|
|
|
|
|
--note-sky: #e3f2fd;
|
|
|
|
|
|
--note-mint: #e0f2f1;
|
|
|
|
|
|
--note-lavender: #f3e5f5;
|
|
|
|
|
|
--note-default: #ffffff;
|
|
|
|
|
|
--teal: #0d9488;
|
|
|
|
|
|
--teal-soft: #ccfbf1;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.panel-backdrop {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
background: rgba(15, 23, 42, 0.45);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.editor-panel {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
width: min(520px, 100%);
|
|
|
|
|
|
max-height: min(92vh, 840px);
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius, 10px);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
background: #fff;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.28);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-09-05 19:59:47 +03:30
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
animation: modal-in 0.2s ease;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
2026-09-05 19:59:47 +03:30
|
|
|
|
@keyframes modal-in {
|
2026-09-05 19:13:01 +03:30
|
|
|
|
from {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
transform: translateY(10px) scale(0.98);
|
|
|
|
|
|
opacity: 0.55;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-head,
|
|
|
|
|
|
.mob-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
padding: 16px;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.panel-head h2 {
|
|
|
|
|
|
margin: 0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
line-height: 1.4;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.panel-scroll {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: auto;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
padding: 16px;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
gap: 16px;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.panel-foot {
|
|
|
|
|
|
display: flex;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 16px;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
border-top: 1px solid #e2e8f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title-input {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
padding: 0.35rem 0;
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
.body-canvas {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border: 0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.85rem;
|
|
|
|
|
|
resize: vertical;
|
|
|
|
|
|
min-height: 160px;
|
|
|
|
|
|
background: var(--canvas, var(--note-sand));
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
font-size: 0.98rem;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.04);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.swatches {
|
|
|
|
|
|
display: flex;
|
2026-09-05 19:41:39 +03:30
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.palette-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 0.65rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.swatch {
|
2026-09-05 19:41:39 +03:30
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
border-radius: 50%;
|
2026-09-05 19:41:39 +03:30
|
|
|
|
border: 1.5px solid rgba(15, 23, 42, 0.18);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-09-05 19:41:39 +03:30
|
|
|
|
color: #0f766e;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
padding: 0;
|
2026-09-05 19:41:39 +03:30
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
.swatch.tone-default {
|
|
|
|
|
|
border-color: #94a3b8;
|
|
|
|
|
|
background-image: linear-gradient(
|
|
|
|
|
|
45deg,
|
|
|
|
|
|
#f1f5f9 25%,
|
|
|
|
|
|
transparent 25%,
|
|
|
|
|
|
transparent 75%,
|
|
|
|
|
|
#f1f5f9 75%
|
|
|
|
|
|
);
|
|
|
|
|
|
background-size: 8px 8px;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.swatch.selected {
|
2026-09-05 19:41:39 +03:30
|
|
|
|
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--teal, #0d9488);
|
|
|
|
|
|
}
|
|
|
|
|
|
.pin-btn.on {
|
|
|
|
|
|
border-color: #f59e0b;
|
|
|
|
|
|
color: #b45309;
|
|
|
|
|
|
background: #fffbeb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:59:47 +03:30
|
|
|
|
.panel-head-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.desktop-only-flex {
|
|
|
|
|
|
display: flex !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mobile-only-flex {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.desktop-fields {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0.75rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.media-row {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
gap: 0.4rem;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
align-items: center;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
|
|
|
|
|
.media-btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.3rem;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
background: #fff;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.35rem 0.55rem;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.media-btn.on {
|
|
|
|
|
|
border-color: var(--teal);
|
|
|
|
|
|
color: var(--teal);
|
|
|
|
|
|
background: var(--teal-soft);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.field,
|
|
|
|
|
|
.field-row .field {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0.3rem;
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
.field select,
|
|
|
|
|
|
.field input[type='text'] {
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.45rem 0.55rem;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.field-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 0.55rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-toggle {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.65rem;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: 0.7rem 0.75rem;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
background: #f0fdfa;
|
|
|
|
|
|
border: 1px solid #99f6e4;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.ai-toggle strong {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.ai-toggle p {
|
|
|
|
|
|
margin: 0.2rem 0 0;
|
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checklist-ed {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.check-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.check-row input[type='text'] {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.35rem 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.linkish {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--teal);
|
|
|
|
|
|
cursor: pointer;
|
2026-09-05 01:45:53 +03:30
|
|
|
|
font-size: 0.85rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
text-align: start;
|
|
|
|
|
|
padding: 0.2rem 0;
|
2026-09-05 01:45:53 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.pending-files {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.file-chip,
|
|
|
|
|
|
.tag-chip {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
display: inline-flex;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
align-items: center;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
gap: 0.3rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
background: #f1f5f9;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.25rem 0.45rem;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
}
|
|
|
|
|
|
.file-chip button {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: #94a3b8;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.btn-primary,
|
|
|
|
|
|
.btn-save {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: var(--teal);
|
|
|
|
|
|
color: #fff;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius, 10px);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.55rem 1rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-primary:disabled,
|
|
|
|
|
|
.btn-save:disabled {
|
|
|
|
|
|
opacity: 0.65;
|
|
|
|
|
|
cursor: wait;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-ghost {
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #475569;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius, 10px);
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 0.55rem 1rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
|
flex: 1;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.mobile-editor {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 90;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mob-canvas {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
padding: 0.85rem 1rem 1rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mob-canvas .body-canvas {
|
|
|
|
|
|
min-height: 42vh;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip-summary {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mob-toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.15rem;
|
|
|
|
|
|
padding: 0.45rem 0.35rem calc(0.55rem + env(safe-area-inset-bottom));
|
|
|
|
|
|
border-top: 1px solid #e2e8f0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mob-toolbar > button,
|
|
|
|
|
|
.voice-slot {
|
|
|
|
|
|
flex: 1 0 auto;
|
|
|
|
|
|
min-width: 3.4rem;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.15rem;
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0.25rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-slot .tiny {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 0.6rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.sheet-root {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 5;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sheet-backdrop {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: rgba(15, 23, 42, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
.sheet {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
background: #fff;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom));
|
|
|
|
|
|
max-height: 85vh;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 0.7rem;
|
|
|
|
|
|
animation: sheet-up 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes sheet-up {
|
|
|
|
|
|
from {
|
|
|
|
|
|
transform: translateY(24px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.sheet header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sheet h3 {
|
2026-08-29 21:06:30 +03:30
|
|
|
|
margin: 0;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sheet-done {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-top: 0.25rem;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.mobile-only {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
@media (max-width: 1100px) {
|
|
|
|
|
|
.notes-board.is-grid {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
2026-09-05 01:45:53 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 19:13:01 +03:30
|
|
|
|
@media (max-width: 860px) {
|
|
|
|
|
|
.desktop-only {
|
|
|
|
|
|
display: none !important;
|
2026-09-05 01:45:53 +03:30
|
|
|
|
}
|
2026-09-05 19:59:47 +03:30
|
|
|
|
.desktop-only-flex,
|
|
|
|
|
|
.desktop-fields {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mobile-only-flex {
|
|
|
|
|
|
display: flex !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip-summary.mobile-only-flex {
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.mobile-quick.mobile-only,
|
|
|
|
|
|
.chips.mobile-only {
|
|
|
|
|
|
display: flex !important;
|
2026-09-05 01:45:53 +03:30
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.fab.mobile-only {
|
|
|
|
|
|
display: inline-flex !important;
|
2026-09-05 01:45:53 +03:30
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.notes-board.is-grid {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 0.65rem;
|
2026-09-05 01:45:53 +03:30
|
|
|
|
}
|
2026-09-05 19:13:01 +03:30
|
|
|
|
.note-card {
|
2026-09-05 19:59:47 +03:30
|
|
|
|
padding: 0.65rem;
|
2026-09-05 19:13:01 +03:30
|
|
|
|
}
|
2026-09-05 19:59:47 +03:30
|
|
|
|
.editor-panel {
|
|
|
|
|
|
width: min(100%, 520px);
|
|
|
|
|
|
max-height: 94vh;
|
2026-09-12 01:33:07 +03:30
|
|
|
|
border-radius: var(--radius);
|
2026-09-05 19:59:47 +03:30
|
|
|
|
}
|
|
|
|
|
|
.panel-root {
|
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-save.mobile-only-flex {
|
|
|
|
|
|
padding: 0.4rem 0.85rem;
|
|
|
|
|
|
font-size: 0.88rem;
|
2026-09-03 14:20:55 +03:30
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
}
|
|
|
|
|
|
</style>
|