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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user