Align older CRM pages with the Mento visual language.

Restyle list, form, settings, and secondary modules to match contacts/activities patterns with teal chrome, soft panels, and mobile layouts — without changing already-redesigned screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 02:29:41 +03:30
co-authored by Cursor
parent 20a9ac0d34
commit 092767b6f2
22 changed files with 3946 additions and 1717 deletions
+192 -57
View File
@@ -4,10 +4,12 @@ import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import type { ProjectCardDto } from '@mento/shared';
import UiIcon from '@/components/UiIcon.vue';
const route = useRoute();
const router = useRouter();
const isNew = computed(() => route.name === 'project-card-new');
const pageTitle = computed(() => (isNew.value ? 'کارت جدید' : 'ویرایش کارت'));
const loading = ref(false);
const saving = ref(false);
@@ -78,42 +80,74 @@ async function removeCard() {
}
}
function cancel() {
void router.push({ name: 'projects' });
}
onMounted(load);
</script>
<template>
<section class="page">
<header class="page-head">
<div>
<RouterLink class="back" :to="{ name: 'projects' }">← بازگشت به برد</RouterLink>
<h1>{{ isNew ? 'کارت جدید' : 'ویرایش کارت' }}</h1>
<header class="desktop-head">
<div class="title-block">
<span class="doc-ico"><UiIcon name="document" :size="22" /></span>
<div>
<h1>{{ pageTitle }}</h1>
<p class="crumb">
<RouterLink :to="{ name: 'projects' }">پروژه‌ها</RouterLink>
<span>›</span>
<strong>{{ pageTitle }}</strong>
</p>
</div>
</div>
<div class="desk-actions">
<button
class="btn primary"
type="button"
:disabled="saving || title.trim().length < 2"
@click="save"
>
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
<button class="btn cancel" type="button" @click="cancel">انصراف</button>
<button v-if="!isNew" class="btn danger" type="button" @click="removeCard">حذف</button>
</div>
</header>
<header class="mobile-head">
<button
class="icon-btn"
type="button"
aria-label="ذخیره"
:disabled="saving || title.trim().length < 2"
@click="save"
>
<UiIcon name="check-plain" :size="22" />
</button>
<h1>{{ pageTitle }}</h1>
<button class="icon-btn" type="button" aria-label="بستن" @click="cancel">
<UiIcon name="x" :size="20" />
</button>
</header>
<p v-if="error" class="error">{{ error }}</p>
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
<form v-else class="form" @submit.prevent="save">
<label>
عنوان
<input v-model="title" type="text" required minlength="2" maxlength="200" />
</label>
<label>
یادداشت
<textarea v-model="notes" rows="5" maxlength="4000" />
</label>
<div class="actions">
<button class="btn" type="submit" :disabled="saving || title.trim().length < 2">
<form v-else class="card" @submit.prevent="save">
<div class="field">
<label for="card-title">عنوان</label>
<input id="card-title" v-model="title" type="text" required minlength="2" maxlength="200" />
</div>
<div class="field">
<label for="card-notes">یادداشت</label>
<textarea id="card-notes" v-model="notes" rows="5" maxlength="4000" />
</div>
<div class="mobile-actions">
<button class="btn primary" type="submit" :disabled="saving || title.trim().length < 2">
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
<button
v-if="!isNew"
class="btn btn-ghost danger"
type="button"
@click="removeCard"
>
حذف
</button>
<button v-if="!isNew" class="btn danger" type="button" @click="removeCard">حذف</button>
</div>
</form>
</section>
@@ -122,67 +156,168 @@ onMounted(load);
<style scoped>
.page {
display: grid;
gap: 1rem;
gap: 0.85rem;
max-width: 640px;
min-width: 0;
width: 100%;
}
.back {
color: var(--muted);
text-decoration: none;
font-size: 0.9rem;
.desktop-head {
display: none;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.title-block {
display: flex;
align-items: center;
gap: 0.75rem;
}
.doc-ico {
width: 44px;
height: 44px;
border-radius: 14px;
display: grid;
place-items: center;
background: #ecfdf5;
color: #0f766e;
}
h1 {
margin: 0.35rem 0 0;
margin: 0;
font-size: 1.35rem;
}
.form {
.crumb {
margin: 0.2rem 0 0;
display: flex;
align-items: center;
gap: 0.35rem;
color: #94a3b8;
font-size: 0.82rem;
}
.crumb a {
color: #0f766e;
font-weight: 700;
text-decoration: none;
}
.desk-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.mobile-head {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
}
.mobile-head h1 {
text-align: center;
font-size: 1.05rem;
}
.icon-btn {
width: 40px;
height: 40px;
border: 0;
background: none;
color: #0f766e;
display: grid;
place-items: center;
cursor: pointer;
}
.card {
display: grid;
gap: 0.85rem;
background: #fff;
border-radius: 18px;
padding: 1rem;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
}
label {
.field {
display: grid;
gap: 0.35rem;
color: var(--muted);
font-size: 0.9rem;
}
.field label {
color: #64748b;
font-size: 0.88rem;
font-weight: 700;
}
input,
textarea {
width: 100%;
min-width: 0;
max-width: 100%;
border: 1px solid var(--line);
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 0.7rem 0.85rem;
font: inherit;
color: var(--ink);
}
.actions {
display: flex;
flex-wrap: wrap;
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-height: 42px;
padding: 0.55rem 1rem;
border-radius: 12px;
font-weight: 700;
border: 1px solid transparent;
cursor: pointer;
font: inherit;
}
.btn.primary {
background: #14b8a6;
color: #fff;
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
}
.btn.cancel {
background: #fff;
color: #64748b;
border-color: #e2e8f0;
}
.btn.danger {
background: #fff;
color: #b91c1c;
border-color: #fecaca;
}
.mobile-actions {
display: grid;
gap: 0.5rem;
}
@media (max-width: 900px) {
h1 {
font-size: 1.35rem;
}
.actions {
flex-direction: column;
align-items: stretch;
}
.actions .btn {
width: 100%;
}
}
.error {
color: #b42318;
color: #b91c1c;
margin: 0;
}
.muted {
color: var(--muted);
color: #64748b;
margin: 0;
}
.danger {
color: #b42318;
@media (min-width: 960px) {
.desktop-head {
display: flex;
}
.mobile-head,
.mobile-actions {
display: none;
}
}
</style>