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
+118 -115
View File
@@ -17,6 +17,13 @@ import { appendSpokenText } from '@/lib/spoken-text';
import UiIcon from '@/components/UiIcon.vue';
import { brand } from '@mento/shared';
import UserAvatar from '@/components/UserAvatar.vue';
import {
UiBadge,
UiButton,
UiInput,
UiPageHeader,
UiTextarea,
} from '@/components/ui';
import type {
ActivityDto,
ActivityPriority,
@@ -338,44 +345,44 @@ watch([id, isNew], () => void load(), { immediate: true });
</script>
<template>
<section class="page" :class="{ embedded }" @click="typeOpen = assigneeOpen = personOpen = false">
<div v-if="embedded" class="embed-actions">
<button class="btn primary" type="button" :disabled="saving" @click="submit('detail')">
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
</button>
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
</div>
<header v-if="!embedded" class="desktop-head">
<p class="crumb">
<RouterLink :to="{ name: 'activities' }">فعالیت‌ها</RouterLink>
<span>‹</span>
<strong>{{ pageTitle }}</strong>
</p>
<div class="desk-actions">
<button class="btn primary" type="button" :disabled="saving" @click="submit('detail')">
<UiIcon name="check-plain" :size="16" />
<section class="crm-page" :class="{ embedded }" @click="typeOpen = assigneeOpen = personOpen = false">
<UiPageHeader
v-if="!embedded"
:title="pageTitle"
:back-to="{ name: 'activities' }"
>
<template #lead>
<span class="crumb">
<RouterLink :to="{ name: 'activities' }">فعالیت‌ها</RouterLink>
<span>›</span>
<strong>{{ pageTitle }}</strong>
</span>
</template>
<template #actions>
<UiButton
variant="primary"
type="button"
:disabled="saving"
:loading="saving"
@click="submit('detail')"
>
<UiIcon name="check" :size="16" />
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
</button>
<button class="btn outline" type="button" :disabled="saving" @click="submit('new')">
</UiButton>
<UiButton
variant="outline"
type="button"
:disabled="saving"
@click="submit('new')"
>
<UiIcon name="plus" :size="16" />
ذخیره و ایجاد جدید
</button>
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">
<UiIcon name="x" :size="16" />
</UiButton>
<UiButton variant="ghost" type="button" :disabled="saving" @click="cancel">
انصراف
</button>
</div>
</header>
<header v-if="!embedded" class="mobile-head">
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit('detail')">
<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>
</UiButton>
</template>
</UiPageHeader>
<nav class="tabs">
<button type="button" :class="{ on: tab === 'main' }" @click="tab = 'main'">اطلاعات اصلی</button>
@@ -410,7 +417,7 @@ watch([id, isNew], () => void load(), { immediate: true });
</div>
<div class="field">
<label for="title">عنوان فعالیت <i>*</i></label>
<input
<UiInput
id="title"
v-model="title"
required
@@ -560,10 +567,10 @@ watch([id, isNew], () => void load(), { immediate: true });
@transcript="(text) => (description = appendSpokenText(description, text))"
/>
</div>
<textarea
<UiTextarea
id="description"
v-model="description"
rows="8"
:rows="8"
placeholder="جزئیات تماس، نتیجه جلسه یا یادداشت مرتبط را بنویسید…"
/>
</div>
@@ -603,114 +610,108 @@ watch([id, isNew], () => void load(), { immediate: true });
</div>
<div>
<dt>وضعیت انجام</dt>
<dd>{{ statusText }}</dd>
<dd>
<UiBadge :variant="isDone ? 'success' : 'neutral'" size="sm">
{{ statusText }}
</UiBadge>
</dd>
</div>
<div>
<dt>اولویت</dt>
<dd>{{ priorityText }}</dd>
<dd>
<UiBadge
:variant="priority === 'very_high' || priority === 'high' ? 'danger' : priority === 'medium' ? 'warning' : 'neutral'"
size="sm"
>
{{ priorityText }}
</UiBadge>
</dd>
</div>
</dl>
</aside>
</div>
<footer v-if="!embedded" class="mobile-save">
<button class="btn primary" type="button" :disabled="saving || loading" @click="submit('detail')">
<UiButton
variant="primary"
type="button"
:disabled="saving || loading"
:loading="saving"
@click="submit('detail')"
>
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
</button>
</UiButton>
</footer>
<!-- Embedded Modal Actions (Fixed at bottom) -->
<div v-if="embedded" class="embed-actions">
<UiButton
variant="primary"
type="button"
:disabled="saving"
:loading="saving"
@click="submit('detail')"
>
<UiIcon name="check" :size="16" />
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
</UiButton>
<UiButton variant="ghost" type="button" :disabled="saving" @click="cancel">انصراف</UiButton>
</div>
</section>
</template>
<style scoped>
.page {
display: grid;
gap: 0.85rem;
padding-bottom: 5.5rem;
}
.page.embedded {
.crm-page.embedded {
padding-bottom: 0;
}
.embed-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.desktop-head {
display: none;
justify-content: space-between;
gap: 1rem;
align-items: center;
gap: var(--sp-2);
margin-bottom: var(--sp-3);
}
.crumb {
margin: 0;
display: flex;
align-items: center;
gap: 0.4rem;
color: #64748b;
gap: var(--sp-2);
color: var(--color-text-muted);
font-size: var(--fs-xs);
}
.crumb a {
color: #0f766e;
color: var(--color-primary-dark);
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 {
margin: 0;
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;
text-decoration: none;
}
.tabs {
display: flex;
gap: 1.2rem;
border-bottom: 1px solid #e2e8f0;
gap: var(--sp-4);
border-bottom: 1px solid var(--color-border);
}
.tabs button {
border: 0;
background: none;
padding: 0.55rem 0;
padding: var(--sp-2) 0;
font: inherit;
color: #94a3b8;
color: var(--color-text-muted);
cursor: pointer;
transition: all var(--transition-fast);
}
.tabs .on {
color: #0f766e;
font-weight: 800;
box-shadow: inset 0 -2px 0 #14b8a6;
color: var(--color-primary);
font-weight: 700;
box-shadow: inset 0 -2px 0 var(--color-primary);
}
.layout {
display: grid;
gap: 0.85rem;
gap: var(--sp-3);
}
.summary {
@@ -720,32 +721,34 @@ watch([id, isNew], () => void load(), { immediate: true });
.form,
.stack {
display: grid;
gap: 0.75rem;
gap: var(--sp-3);
}
.card {
position: relative;
background: #fff;
border-radius: 18px;
padding: 0.95rem;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
padding: var(--sp-4);
box-shadow: var(--shadow-sm);
}
.field {
display: grid;
gap: 0.4rem;
gap: var(--sp-1);
min-width: 0;
}
.field label,
.pri-label {
margin: 0;
color: #64748b;
font-size: 0.82rem;
color: var(--color-text-muted);
font-size: var(--fs-xs);
font-weight: 600;
}
.field i {
color: #dc2626;
color: var(--color-danger);
font-style: normal;
}
@@ -830,7 +833,7 @@ watch([id, isNew], () => void load(), { immediate: true });
}
.who b {
font-weight: 800;
font-weight: 600;
}
.who small {
@@ -847,7 +850,7 @@ watch([id, isNew], () => void load(), { immediate: true });
padding: 0.35rem;
list-style: none;
background: #fff;
border-radius: 12px;
border-radius: var(--radius);
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
max-height: 16rem;
overflow: auto;
@@ -861,7 +864,7 @@ watch([id, isNew], () => void load(), { immediate: true });
border: 0;
background: none;
padding: 0.55rem 0.5rem;
border-radius: 8px;
border-radius: var(--radius);
font: inherit;
text-align: start;
cursor: pointer;
@@ -937,7 +940,7 @@ watch([id, isNew], () => void load(), { immediate: true });
width: 28px;
height: 28px;
border: 0;
border-radius: 10px;
border-radius: var(--radius);
background: #f0fdfa;
color: #0f766e;
display: grid;
@@ -1005,7 +1008,7 @@ watch([id, isNew], () => void load(), { immediate: true });
}
.pri.on span {
font-weight: 800;
font-weight: 600;
}
.reminder :deep(.trigger) {
@@ -1026,7 +1029,7 @@ watch([id, isNew], () => void load(), { immediate: true });
.channel-title {
margin: 0;
font-size: 0.88rem;
font-weight: 800;
font-weight: 600;
color: #0f172a;
}
@@ -1036,7 +1039,7 @@ watch([id, isNew], () => void load(), { immediate: true });
gap: 0.55rem;
cursor: pointer;
padding: 0.55rem 0.65rem;
border-radius: 12px;
border-radius: var(--radius);
border: 1px solid #e2e8f0;
background: #f8fafc;
}
@@ -1109,7 +1112,7 @@ watch([id, isNew], () => void load(), { immediate: true });
border-radius: var(--radius);
padding: 0 0.95rem;
font: inherit;
font-weight: 800;
font-weight: 600;
cursor: pointer;
}
@@ -1176,7 +1179,7 @@ watch([id, isNew], () => void load(), { immediate: true });
position: sticky;
top: 0.8rem;
background: #fff;
border-radius: 20px;
border-radius: var(--radius);
padding: 1rem 1.05rem 0.7rem;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
}
@@ -1195,7 +1198,7 @@ watch([id, isNew], () => void load(), { immediate: true });
justify-items: center;
gap: 0.45rem;
padding: 1rem 0.6rem;
border-radius: 14px;
border-radius: var(--radius);
background: #f8fafc;
color: #94a3b8;
text-align: center;