feat(ui): standardize design system, modals, sidebar, and form inputs

- Implement unified design system tokens, 10px radius, and IRANSans typography
- Modernize sidebar with Gemini-style collapsed expander, square 1:1 items, and elevated profile flyout
- Standardize all modal dialogs with 16px padding, fixed header/footer, 16px medium title, and sticky bottom actions
- Refactor DealFormView and CustomFieldsSection with standard UiField, UiInput, and UiSelect components
- Add draggable bottom-left QuickCreate floating action button
This commit is contained in:
s.esmaeili
2026-09-12 01:33:07 +03:30
parent 9a991fc1f1
commit 1ae87c97b7
87 changed files with 7844 additions and 5718 deletions
+153 -259
View File
@@ -6,6 +6,14 @@ import { api } from '@/lib/api';
import type { CompanyDto, CreateCompanyRequest } from '@mento/shared';
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
import EntityTimeline from '@/components/EntityTimeline.vue';
import {
UiButton,
UiCard,
UiField,
UiInput,
UiPageHeader,
UiTextarea,
} from '@/components/ui';
import UiIcon from '@/components/UiIcon.vue';
const props = withDefaults(
@@ -134,301 +142,187 @@ onMounted(load);
</script>
<template>
<section class="page" :class="{ embedded }">
<div v-if="embedded" class="embed-actions">
<button class="btn primary" type="button" :disabled="saving" @click="submit">
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
</div>
<header v-if="!embedded" class="desktop-head">
<div class="title-block">
<span class="doc-ico"><UiIcon name="building" :size="22" /></span>
<div>
<h1>{{ pageTitle }}</h1>
<p class="crumb">
<RouterLink :to="{ name: 'contacts', query: { tab: 'companies' } }">شرکت‌ها</RouterLink>
<span>›</span>
<strong>{{ pageTitle }}</strong>
</p>
</div>
</div>
<div class="desk-actions">
<button class="btn primary" type="button" :disabled="saving" @click="submit">
<UiIcon name="check-plain" :size="16" />
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
<button v-if="!isNew" class="btn danger" type="button" :disabled="saving" @click="remove">
<section class="crm-page company-form-page" :class="{ embedded }" dir="rtl">
<UiPageHeader
v-if="!embedded"
:title="pageTitle"
description="ثبت و ویرایش مشخصات حقوقی، اطلاعات تماس و فیلدهای سفارشی شرکت"
>
<template #breadcrumb>
<RouterLink :to="{ name: 'contacts', query: { tab: 'companies' } }" class="breadcrumb-link">
شرکت‌ها
</RouterLink>
<span class="breadcrumb-separator">/</span>
<span>{{ pageTitle }}</span>
</template>
<template #actions>
<UiButton :loading="saving" @click="submit">
<template #icon>
<UiIcon name="check-plain" />
</template>
ذخیره
</UiButton>
<UiButton variant="ghost" :disabled="saving" @click="cancel">
انصراف
</UiButton>
<UiButton v-if="!isNew" variant="danger" :disabled="saving" @click="remove">
<template #icon>
<UiIcon name="trash" />
</template>
حذف
</button>
</div>
</header>
</UiButton>
</template>
</UiPageHeader>
<header v-if="!embedded" class="mobile-head">
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit">
<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>
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
<p v-if="loading" class="muted">در حال بارگذاری…</p>
<form v-else class="form" @submit.prevent="submit">
<section class="card">
<div class="grid">
<div class="field wide">
<label for="name">نام شرکت</label>
<input id="name" v-model="name" required minlength="2" />
</div>
<div class="field">
<label for="nationalId">شناسه ملی</label>
<input id="nationalId" v-model="nationalId" />
</div>
<div class="field">
<label for="phone">تلفن</label>
<input id="phone" v-model="phone" inputmode="tel" />
</div>
<div class="field">
<label for="email">ایمیل</label>
<input id="email" v-model="email" type="email" />
</div>
<div class="field">
<label for="website">وب‌سایت</label>
<input id="website" v-model="website" />
</div>
<div class="field wide">
<label for="address">آدرس</label>
<input id="address" v-model="address" />
</div>
<div class="field wide">
<label for="notes">یادداشت</label>
<textarea id="notes" v-model="notes" rows="4" />
</div>
<form class="crm-form" @submit.prevent="submit">
<UiCard title="اطلاعات اصلی شرکت" class="section-card">
<div class="crm-form-grid-2">
<UiField label="نام شرکت" required class="col-span-2">
<UiInput
id="name"
v-model="name"
required
minlength="2"
placeholder="مثال: داده‌پردازی سپهر"
/>
</UiField>
<UiField label="شناسه ملی">
<UiInput
id="nationalId"
v-model="nationalId"
placeholder="شناسه ملی ۱۱ رقمی"
dir="ltr"
/>
</UiField>
<UiField label="شماره تلفن شرکت">
<UiInput
id="phone"
v-model="phone"
type="tel"
placeholder="۰۲۱..."
dir="ltr"
/>
</UiField>
<UiField label="پست الکترونیکی (ایمیل)">
<UiInput
id="email"
v-model="email"
type="email"
placeholder="info@company.ir"
dir="ltr"
/>
</UiField>
<UiField label="وب‌سایت">
<UiInput
id="website"
v-model="website"
placeholder="https://..."
dir="ltr"
/>
</UiField>
<UiField label="آدرس پستی" class="col-span-2">
<UiInput
id="address"
v-model="address"
placeholder="نشانی، طبقه، پلاک..."
/>
</UiField>
<UiField label="یادداشت و توضیحات تکمیلی" class="col-span-2">
<UiTextarea
id="notes"
v-model="notes"
:rows="4"
placeholder="توضیحات مهم، حوزه‌های همکاری..."
/>
</UiField>
</div>
</section>
</UiCard>
<CustomFieldsSection entity-type="company" v-model="customFields" />
<EntityTimeline v-if="!isNew && id" entity-type="company" :entity-id="id" />
<p v-if="error" class="error">{{ error }}</p>
<div class="mobile-actions">
<button class="btn primary" type="submit" :disabled="saving">
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
<button v-if="!isNew" class="btn danger" type="button" :disabled="saving" @click="remove">
حذف
</button>
<div class="form-bottom-actions">
<UiButton type="submit" :loading="saving">
<template #icon>
<UiIcon name="check-plain" />
</template>
ذخیره اطلاعات
</UiButton>
<UiButton variant="ghost" type="button" :disabled="saving" @click="cancel">
انصراف
</UiButton>
<UiButton v-if="!isNew" variant="danger" type="button" :disabled="saving" @click="remove">
حذف شرکت
</UiButton>
</div>
</form>
</section>
</template>
<style scoped>
.page {
display: grid;
gap: 0.85rem;
max-width: 820px;
min-width: 0;
padding-bottom: 1.5rem;
}
.page.embedded {
max-width: none;
padding-bottom: 0;
}
.embed-actions {
.company-form-page {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.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;
font-size: 1.35rem;
}
.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;
}
.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: var(--control-h);
height: var(--control-h);
border: 0;
background: none;
color: #0f766e;
display: grid;
place-items: center;
cursor: pointer;
}
.form {
display: grid;
gap: 0.75rem;
}
.card {
background: #fff;
border-radius: 18px;
padding: 0.95rem;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.field {
display: grid;
gap: 0.35rem;
}
.field.wide {
grid-column: 1 / -1;
}
.field label {
color: #64748b;
font-size: 0.88rem;
font-weight: 700;
}
.field input,
.field select,
.field textarea {
flex-direction: column;
gap: var(--sp-6);
width: 100%;
border: 1px solid #e2e8f0;
border-radius: var(--radius);
min-height: var(--control-h);
padding: 0 0.85rem;
font: inherit;
background: #fff;
box-sizing: border-box;
max-width: 100%;
}
.field textarea {
min-height: 5.5rem;
padding: 0.65rem 0.85rem;
.company-form-page.embedded {
max-width: none;
gap: var(--sp-4);
}
.muted {
color: #64748b;
margin: 0;
.embedded-actions {
display: flex;
gap: var(--sp-2);
}
.error {
color: #b91c1c;
margin: 0;
.breadcrumb-link {
color: var(--color-text-secondary);
text-decoration: none;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-height: var(--control-h);
padding: 0 0.95rem;
border-radius: var(--radius);
font-weight: 700;
border: 1px solid transparent;
cursor: pointer;
font: inherit;
.breadcrumb-link:hover {
color: var(--color-brand-600);
}
.btn.primary {
background: #14b8a6;
color: #fff;
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
.breadcrumb-separator {
margin: 0 var(--sp-2);
color: var(--color-text-muted);
}
.btn.cancel {
background: #fff;
color: #64748b;
border-color: #e2e8f0;
.section-card {
padding: var(--sp-6);
}
.btn.danger {
background: #fff;
color: #b91c1c;
border-color: #fecaca;
.col-span-2 {
grid-column: span 2;
}
.mobile-actions {
display: grid;
gap: 0.5rem;
.form-bottom-actions {
display: flex;
gap: var(--sp-3);
justify-content: flex-end;
margin-top: var(--sp-4);
}
@media (min-width: 960px) {
.desktop-head {
display: flex;
@media (max-width: 640px) {
.col-span-2 {
grid-column: span 1;
}
.mobile-head,
.mobile-actions {
display: none;
}
}
@media (max-width: 700px) {
.grid {
grid-template-columns: 1fr;
.form-bottom-actions {
flex-direction: column;
}
}
</style>