Merge design_ui_v2 into design_ui so the new design system can be previewed without dropping later product work.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -90,7 +90,7 @@ form {
|
||||
gap: 0.45rem;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.35rem 0.4rem 0.35rem 0.7rem;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ input {
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
@@ -126,7 +126,7 @@ button:disabled {
|
||||
|
||||
.answer {
|
||||
background: #f0fdfa;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.7rem 0.85rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { api } from '@/lib/api';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import { UiField, UiInput, UiSelect } from '@/components/ui';
|
||||
import type {
|
||||
CustomFieldDefinitionDto,
|
||||
CustomFieldEntity,
|
||||
@@ -58,27 +59,28 @@ onMounted(load);
|
||||
<template>
|
||||
<div v-if="loading" class="muted">بارگذاری فیلدهای سفارشی…</div>
|
||||
<div v-else-if="fields.length" class="custom-block">
|
||||
<h3>فیلدهای سفارشی</h3>
|
||||
<div class="grid">
|
||||
<div v-for="field in fields" :key="field.id" class="field">
|
||||
<label :for="`cf-${field.key}`">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="req">*</span>
|
||||
</label>
|
||||
|
||||
<select
|
||||
<h3 class="custom-title">فیلدهای سفارشی</h3>
|
||||
<div class="crm-form-grid-2">
|
||||
<UiField
|
||||
v-for="field in fields"
|
||||
:key="field.id"
|
||||
:label="field.label"
|
||||
:required="field.required"
|
||||
:for-id="`cf-${field.key}`"
|
||||
>
|
||||
<UiSelect
|
||||
v-if="field.fieldType === 'select'"
|
||||
:id="`cf-${field.key}`"
|
||||
:value="String(displayValue(field.key))"
|
||||
@change="setValue(field.key, ($event.target as HTMLSelectElement).value)"
|
||||
:model-value="String(displayValue(field.key))"
|
||||
@update:model-value="setValue(field.key, $event)"
|
||||
>
|
||||
<option value="">انتخاب کنید</option>
|
||||
<option v-for="opt in field.options" :key="opt" :value="opt">
|
||||
{{ opt }}
|
||||
</option>
|
||||
</select>
|
||||
</UiSelect>
|
||||
|
||||
<label v-else-if="field.fieldType === 'boolean'" class="check">
|
||||
<label v-else-if="field.fieldType === 'boolean'" class="cf-check">
|
||||
<input
|
||||
:id="`cf-${field.key}`"
|
||||
type="checkbox"
|
||||
@@ -87,20 +89,18 @@ onMounted(load);
|
||||
setValue(field.key, ($event.target as HTMLInputElement).checked)
|
||||
"
|
||||
/>
|
||||
فعال
|
||||
<span>فعال</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
<UiInput
|
||||
v-else-if="field.fieldType === 'number'"
|
||||
:id="`cf-${field.key}`"
|
||||
type="number"
|
||||
:value="String(displayValue(field.key))"
|
||||
@input="
|
||||
:model-value="props.modelValue[field.key] != null ? Number(props.modelValue[field.key]) : ''"
|
||||
@update:model-value="
|
||||
setValue(
|
||||
field.key,
|
||||
($event.target as HTMLInputElement).value === ''
|
||||
? null
|
||||
: Number(($event.target as HTMLInputElement).value),
|
||||
$event === '' || $event == null ? null : Number($event),
|
||||
)
|
||||
"
|
||||
/>
|
||||
@@ -112,66 +112,55 @@ onMounted(load);
|
||||
@update:model-value="setValue(field.key, $event || null)"
|
||||
/>
|
||||
|
||||
<input
|
||||
<UiInput
|
||||
v-else
|
||||
:id="`cf-${field.key}`"
|
||||
type="text"
|
||||
:value="String(displayValue(field.key))"
|
||||
@input="setValue(field.key, ($event.target as HTMLInputElement).value)"
|
||||
:model-value="String(displayValue(field.key))"
|
||||
@update:model-value="setValue(field.key, $event)"
|
||||
/>
|
||||
</div>
|
||||
</UiField>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-block {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
padding-top: var(--sp-4);
|
||||
border-top: 1px dashed var(--line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
.custom-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.9rem;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: var(--fw-semibold);
|
||||
color: var(--ink-secondary);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.req {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.check {
|
||||
display: flex;
|
||||
.cf-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
gap: var(--sp-2);
|
||||
min-height: var(--control-h-md);
|
||||
font-size: var(--fs-base);
|
||||
font-weight: var(--fw-medium);
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type='text'],
|
||||
input[type='number'] {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 0.95rem;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.cf-check input[type='checkbox'] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -289,7 +289,7 @@ onMounted(load);
|
||||
.composer input {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.7rem 0.85rem;
|
||||
background: #fff;
|
||||
}
|
||||
@@ -316,7 +316,7 @@ onMounted(load);
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 0.85rem;
|
||||
background: #fbfcfb;
|
||||
}
|
||||
|
||||
@@ -675,7 +675,7 @@ onBeforeUnmount(() => {
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
min-height: 52px;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.45rem 0.85rem 0.45rem 0.45rem;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fffd 100%);
|
||||
}
|
||||
@@ -698,7 +698,7 @@ onBeforeUnmount(() => {
|
||||
left: 50%;
|
||||
bottom: 50%;
|
||||
transform-origin: center bottom;
|
||||
border-radius: 99px;
|
||||
border-radius: var(--radius);
|
||||
background: #0f766e;
|
||||
}
|
||||
|
||||
@@ -726,7 +726,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.clock-text {
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 1.05rem;
|
||||
@@ -768,7 +768,7 @@ onBeforeUnmount(() => {
|
||||
max-height: calc(100vh - 16px);
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
||||
padding: 0.75rem 0.8rem 0.7rem;
|
||||
font-family: IRANSans, Tahoma, sans-serif;
|
||||
@@ -797,7 +797,7 @@ onBeforeUnmount(() => {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
background: #f1f5f9;
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
@@ -826,7 +826,7 @@ onBeforeUnmount(() => {
|
||||
.day {
|
||||
height: 34px;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
border-radius: var(--radius);
|
||||
background: none;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
@@ -881,12 +881,12 @@ onBeforeUnmount(() => {
|
||||
min-width: 5.1rem;
|
||||
height: 4.1rem;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius);
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
font-size: 2.35rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.02em;
|
||||
cursor: pointer;
|
||||
@@ -900,7 +900,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.md-colon {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
line-height: 1;
|
||||
padding-bottom: 0.15rem;
|
||||
@@ -1005,7 +1005,7 @@ onBeforeUnmount(() => {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: #0f766e;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
@@ -1033,7 +1033,7 @@ onBeforeUnmount(() => {
|
||||
height: 220px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
padding: 0.25rem;
|
||||
scrollbar-width: thin;
|
||||
@@ -1044,7 +1044,7 @@ onBeforeUnmount(() => {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
background: transparent;
|
||||
color: #334155;
|
||||
font: inherit;
|
||||
@@ -1087,9 +1087,9 @@ onBeforeUnmount(() => {
|
||||
margin-inline-start: auto;
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ h2 {
|
||||
|
||||
textarea {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 0.9rem;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
@@ -221,7 +221,7 @@ textarea {
|
||||
|
||||
.items li {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 0.85rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ function formatPayloadSummary(value: NoteAiPayloadDto | null) {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.75rem 0.85rem;
|
||||
border: 1px dashed var(--accent);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
@@ -281,7 +281,7 @@ header {
|
||||
input[type='text'],
|
||||
textarea {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.55rem 0.7rem;
|
||||
font: inherit;
|
||||
width: 100%;
|
||||
|
||||
@@ -5,8 +5,9 @@ import type { RouteLocationRaw } from 'vue-router';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
const FAB_SIZE = 36;
|
||||
const FAB_MARGIN = 20;
|
||||
const POS_KEY = 'mento.quickCreateFab';
|
||||
const FAB_MARGIN_BOTTOM = 24;
|
||||
const FAB_MARGIN_LEFT = 24;
|
||||
const POS_KEY = 'mento.quickCreateFab_v2';
|
||||
|
||||
type FabPos = { left: number; top: number };
|
||||
|
||||
@@ -67,7 +68,10 @@ const router = useRouter();
|
||||
const hidden = computed(() => route.name === 'person-detail');
|
||||
const isQuickCreateOpen = ref(false);
|
||||
const ready = ref(false);
|
||||
const fabPos = ref<FabPos>({ left: FAB_MARGIN, top: FAB_MARGIN });
|
||||
const fabPos = ref<FabPos>({
|
||||
left: FAB_MARGIN_LEFT,
|
||||
top: typeof window !== 'undefined' ? window.innerHeight - FAB_MARGIN_BOTTOM - FAB_SIZE : 600,
|
||||
});
|
||||
const viewport = ref({ w: 1280, h: 800 });
|
||||
const dragging = ref(false);
|
||||
let dragMoved = false;
|
||||
@@ -75,10 +79,10 @@ let dragOffsetX = 0;
|
||||
let dragOffsetY = 0;
|
||||
|
||||
function defaultPos(): FabPos {
|
||||
const bottomGap = viewport.value.w <= 900 ? FAB_MARGIN + 64 : FAB_MARGIN;
|
||||
const extra = viewport.value.w <= 900 ? 64 : 0;
|
||||
return {
|
||||
left: FAB_MARGIN,
|
||||
top: Math.max(FAB_MARGIN, viewport.value.h - bottomGap - FAB_SIZE),
|
||||
left: FAB_MARGIN_LEFT,
|
||||
top: Math.max(8, viewport.value.h - FAB_MARGIN_BOTTOM - extra - FAB_SIZE),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,7 +175,7 @@ const fabStyle = computed(() => ({
|
||||
const panelStyle = computed(() => {
|
||||
const gap = 12;
|
||||
const edge = 16;
|
||||
const width = Math.min(390, viewport.value.w - 32);
|
||||
const width = Math.min(300, viewport.value.w - 32);
|
||||
let left = fabPos.value.left;
|
||||
left = Math.min(left, viewport.value.w - width - edge);
|
||||
left = Math.max(edge, left);
|
||||
@@ -278,7 +282,7 @@ onBeforeUnmount(() => {
|
||||
@click="go(item.to)"
|
||||
>
|
||||
<span class="item-ico" :class="item.tone">
|
||||
<UiIcon :name="item.icon" :size="26" />
|
||||
<UiIcon :name="item.icon" :size="18" />
|
||||
</span>
|
||||
<span class="copy">
|
||||
<strong>{{ item.title }}</strong>
|
||||
@@ -303,7 +307,7 @@ onBeforeUnmount(() => {
|
||||
@pointercancel="onFabPointerUp"
|
||||
>
|
||||
<span class="fab-face" aria-hidden="true">
|
||||
<UiIcon :name="isQuickCreateOpen ? 'x' : 'apps-plus'" :size="28" />
|
||||
<UiIcon :name="isQuickCreateOpen ? 'x' : 'apps-plus'" :size="20" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -328,25 +332,26 @@ onBeforeUnmount(() => {
|
||||
.panel {
|
||||
position: fixed;
|
||||
z-index: 92;
|
||||
width: 390px;
|
||||
max-height: min(540px, calc(100dvh - 80px));
|
||||
width: 300px;
|
||||
max-height: min(480px, calc(100dvh - 80px));
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 28px;
|
||||
padding: 16px 16px 12px;
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 12px 10px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
flex-shrink: 0;
|
||||
margin: 0 0 12px;
|
||||
margin: 0 0 10px;
|
||||
text-align: right;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
font-size: var(--fs-md);
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.items {
|
||||
@@ -354,31 +359,37 @@ onBeforeUnmount(() => {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
padding-left: 2px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
min-height: 68px;
|
||||
min-height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #eef1f2;
|
||||
border-radius: 16px;
|
||||
gap: 10px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--line-light);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.04);
|
||||
box-shadow: var(--shadow-xs);
|
||||
font: inherit;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.item-ico {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: var(--radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
@@ -390,7 +401,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.item-ico.company {
|
||||
background: #22d3ee;
|
||||
background: #0891b2;
|
||||
}
|
||||
|
||||
.item-ico.deal {
|
||||
@@ -398,7 +409,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.item-ico.activity {
|
||||
background: #f97316;
|
||||
background: #ea580c;
|
||||
}
|
||||
|
||||
.item-ico.note {
|
||||
@@ -407,33 +418,34 @@ onBeforeUnmount(() => {
|
||||
|
||||
.copy {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.copy strong {
|
||||
font-size: 16px;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
color: var(--ink);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.copy small {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--muted);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
z-index: 93;
|
||||
box-sizing: border-box;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: var(--control-h);
|
||||
height: var(--control-h);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
background: linear-gradient(145deg, #0f766e 0%, #14b8a6 100%);
|
||||
color: #fff;
|
||||
@@ -442,7 +454,7 @@ onBeforeUnmount(() => {
|
||||
line-height: 0;
|
||||
direction: ltr;
|
||||
box-shadow:
|
||||
0 8px 22px rgba(13, 148, 136, 0.4),
|
||||
0 4px 14px rgba(13, 148, 136, 0.35),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
@@ -463,29 +475,28 @@ onBeforeUnmount(() => {
|
||||
|
||||
.fab :deep(.ico) {
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
max-width: 28px;
|
||||
max-height: 28px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
stroke-width: 1.7;
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
.fab:hover {
|
||||
filter: brightness(1.06);
|
||||
box-shadow:
|
||||
0 10px 26px rgba(13, 148, 136, 0.48),
|
||||
0 6px 18px rgba(13, 148, 136, 0.42),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.fab.open {
|
||||
background: linear-gradient(145deg, #115e59 0%, #0d9488 100%);
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.28);
|
||||
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.25);
|
||||
}
|
||||
|
||||
.fab.dragging {
|
||||
cursor: grabbing;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 12px 28px rgba(13, 148, 136, 0.5);
|
||||
box-shadow: 0 8px 24px rgba(13, 148, 136, 0.45);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -64,7 +64,13 @@ defineProps<{
|
||||
| 'logout'
|
||||
| 'pulse'
|
||||
| 'sliders'
|
||||
| 'camera';
|
||||
| 'camera'
|
||||
| 'arrow-right'
|
||||
| 'arrow-left'
|
||||
| 'chevron-right'
|
||||
| 'chevron-left'
|
||||
| 'chevrons-right'
|
||||
| 'chevrons-left';
|
||||
size?: number;
|
||||
}>();
|
||||
</script>
|
||||
@@ -218,9 +224,24 @@ defineProps<{
|
||||
<template v-else-if="name === 'plus'">
|
||||
<path d="M12 6v12M6 12h12" />
|
||||
</template>
|
||||
<template v-else-if="name === 'chevron'">
|
||||
<template v-else-if="name === 'chevron' || name === 'chevron-right'">
|
||||
<path d="m9 6 6 6-6 6" />
|
||||
</template>
|
||||
<template v-else-if="name === 'chevron-left'">
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</template>
|
||||
<template v-else-if="name === 'chevrons-right'">
|
||||
<path d="m6 17 5-5-5-5M13 17l5-5-5-5" />
|
||||
</template>
|
||||
<template v-else-if="name === 'chevrons-left'">
|
||||
<path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
|
||||
</template>
|
||||
<template v-else-if="name === 'arrow-right'">
|
||||
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||
</template>
|
||||
<template v-else-if="name === 'arrow-left'">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||
</template>
|
||||
<template v-else-if="name === 'star'">
|
||||
<path d="m12 3.4 2.4 4.9 5.4.8-3.9 3.8.9 5.4L12 15.8 7.2 18.3l.9-5.4-3.9-3.8 5.4-.8L12 3.4Z" />
|
||||
</template>
|
||||
|
||||
@@ -52,7 +52,7 @@ img {
|
||||
em {
|
||||
font-style: normal;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
font-size: 0.62rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ onBeforeUnmount(() => {
|
||||
.meter i {
|
||||
display: block;
|
||||
width: 3px;
|
||||
border-radius: 99px;
|
||||
border-radius: var(--radius);
|
||||
background: #5eead4;
|
||||
box-shadow: 0 0 8px rgba(94, 234, 212, 0.45);
|
||||
transition: height 70ms linear;
|
||||
@@ -402,7 +402,7 @@ onBeforeUnmount(() => {
|
||||
height: 38px;
|
||||
min-height: 38px;
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
border-color: transparent;
|
||||
background: #f8fafc;
|
||||
justify-content: center;
|
||||
|
||||
@@ -296,8 +296,8 @@ onBeforeUnmount(() => {
|
||||
.head h2 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ onBeforeUnmount(() => {
|
||||
align-items: flex-start;
|
||||
padding: 0.9rem 0.95rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 18px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ onBeforeUnmount(() => {
|
||||
.ctx-copy h3 {
|
||||
margin: 0.1rem 0 0;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.editor {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
min-height: 11.5rem;
|
||||
display: flex;
|
||||
@@ -448,13 +448,13 @@ onBeforeUnmount(() => {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font: inherit;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -473,7 +473,7 @@ onBeforeUnmount(() => {
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
padding: 0.42rem 0.65rem;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
|
||||
@@ -497,7 +497,7 @@ onBeforeUnmount(() => {
|
||||
max-height: 12rem;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.16);
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -510,7 +510,7 @@ onBeforeUnmount(() => {
|
||||
text-align: start;
|
||||
font: inherit;
|
||||
padding: 0.55rem 0.65rem;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
color: #0f172a;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -536,7 +536,7 @@ onBeforeUnmount(() => {
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.72rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: #ecfdf5;
|
||||
color: #0f766e;
|
||||
font-size: 0.8rem;
|
||||
@@ -559,7 +559,7 @@ onBeforeUnmount(() => {
|
||||
height: var(--control-h);
|
||||
border-radius: var(--radius);
|
||||
font: inherit;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@ onBeforeUnmount(() => {
|
||||
position: relative;
|
||||
max-width: 480px;
|
||||
height: min(92dvh, 860px);
|
||||
border-radius: 22px;
|
||||
border-radius: var(--radius, 10px);
|
||||
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,8 +274,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.titles h2 {
|
||||
font-size: 1.02rem;
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ onBeforeUnmount(() => {
|
||||
min-height: 72px;
|
||||
padding: 0.7rem 0.9rem;
|
||||
border: 1px solid #e8eef3;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
@@ -340,7 +340,7 @@ onBeforeUnmount(() => {
|
||||
.label,
|
||||
.copy strong {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ onBeforeUnmount(() => {
|
||||
.ico-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
background: #e6f7f4;
|
||||
color: #0d9488;
|
||||
display: grid;
|
||||
@@ -375,13 +375,13 @@ onBeforeUnmount(() => {
|
||||
.when {
|
||||
color: #0d9488;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.custom-box {
|
||||
border: 1px solid #e8eef3;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.85rem 0.9rem;
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
@@ -419,7 +419,7 @@ onBeforeUnmount(() => {
|
||||
align-items: flex-start;
|
||||
gap: 0.55rem;
|
||||
padding: 0.85rem 0.9rem;
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 0.8rem;
|
||||
@@ -452,7 +452,7 @@ onBeforeUnmount(() => {
|
||||
position: relative;
|
||||
max-width: 480px;
|
||||
height: min(92dvh, 860px);
|
||||
border-radius: 22px;
|
||||
border-radius: var(--radius, 10px);
|
||||
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ withDefaults(
|
||||
.brand-copy strong {
|
||||
font-size: 1.2rem;
|
||||
color: #0f172a;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-copy span {
|
||||
@@ -117,7 +117,7 @@ withDefaults(
|
||||
aspect-ratio: 1.35;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border-radius: 18px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.1);
|
||||
padding: 1rem;
|
||||
backdrop-filter: blur(6px);
|
||||
@@ -150,7 +150,7 @@ withDefaults(
|
||||
|
||||
.chart-line {
|
||||
height: 54px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background:
|
||||
linear-gradient(180deg, transparent 55%, rgba(13, 148, 136, 0.12) 55%),
|
||||
radial-gradient(circle at 20% 60%, #14b8a6 3px, transparent 4px),
|
||||
@@ -184,7 +184,7 @@ withDefaults(
|
||||
position: absolute;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ function nodeClass(step: AutomationStep) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 22rem;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
overflow: auto;
|
||||
background-color: #f3f6f5;
|
||||
background-image: radial-gradient(#c9d4cf 1.1px, transparent 1.2px);
|
||||
@@ -254,7 +254,7 @@ function nodeClass(step: AutomationStep) {
|
||||
gap: 0.7rem;
|
||||
text-align: start;
|
||||
padding: 0.72rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid #d5ddd8;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(23, 33, 28, 0.06);
|
||||
@@ -273,7 +273,7 @@ function nodeClass(step: AutomationStep) {
|
||||
}
|
||||
|
||||
.node.action:not(.condition) {
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
border-inline-start-width: 5px;
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ function nodeClass(step: AutomationStep) {
|
||||
.icon {
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
@@ -350,7 +350,7 @@ function nodeClass(step: AutomationStep) {
|
||||
}
|
||||
|
||||
.node.condition .icon {
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
color: #b45309;
|
||||
background: #ffedd5;
|
||||
}
|
||||
@@ -369,7 +369,7 @@ function nodeClass(step: AutomationStep) {
|
||||
|
||||
strong {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
@@ -472,7 +472,7 @@ em {
|
||||
margin-top: 1rem;
|
||||
background: #fff;
|
||||
border: 1px solid #d5ddd8;
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.85rem;
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
|
||||
@@ -278,14 +278,14 @@ watch(
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hint {
|
||||
@@ -312,7 +312,7 @@ input,
|
||||
select,
|
||||
textarea {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.55rem 0.7rem;
|
||||
font: inherit;
|
||||
color: #0f172a;
|
||||
|
||||
@@ -64,7 +64,7 @@ withDefaults(
|
||||
width: max-content;
|
||||
max-width: 118px;
|
||||
padding: 0.48rem 0.65rem 0.55rem;
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font-size: 0.68rem;
|
||||
|
||||
@@ -56,7 +56,7 @@ withDefaults(
|
||||
letter-spacing: -0.02em;
|
||||
color: #0f172a;
|
||||
line-height: 1.1;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text small {
|
||||
|
||||
@@ -93,7 +93,7 @@ function pick(label: string) {
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1rem 0.95rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
@@ -144,7 +144,7 @@ h2 {
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.58rem 0.7rem;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
border: 1px solid #eef2f7;
|
||||
color: #334155;
|
||||
@@ -168,7 +168,7 @@ h2 {
|
||||
.chip-ico {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
background: #ecfdf8;
|
||||
color: #14b8a6;
|
||||
display: grid;
|
||||
@@ -185,7 +185,7 @@ h2 {
|
||||
.ai-input input {
|
||||
flex: 1;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.58rem 0.75rem;
|
||||
background: #f8fafc;
|
||||
font-size: 0.8rem;
|
||||
@@ -194,7 +194,7 @@ h2 {
|
||||
.ai-input button {
|
||||
width: 40px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
@@ -208,7 +208,7 @@ h2 {
|
||||
|
||||
.ai-answer {
|
||||
background: #f0fdfa;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.7rem 0.8rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ function alertIcon(tone: string) {
|
||||
align-items: start;
|
||||
min-height: 0;
|
||||
padding: 0.8rem 1.5rem 0.9rem;
|
||||
border-radius: 28px;
|
||||
border-radius: var(--radius);
|
||||
overflow: visible;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
@@ -151,7 +151,7 @@ function alertIcon(tone: string) {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
border-radius: 28px;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
background-color: #134e4a;
|
||||
background-image:
|
||||
@@ -251,7 +251,7 @@ function alertIcon(tone: string) {
|
||||
.greet-text h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.35rem, 2vw, 1.75rem);
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -276,7 +276,7 @@ function alertIcon(tone: string) {
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
text-decoration: none;
|
||||
@@ -311,7 +311,7 @@ function alertIcon(tone: string) {
|
||||
.chip-ico {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
@@ -369,7 +369,7 @@ function alertIcon(tone: string) {
|
||||
color: #94a3b8;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.14rem 0.38rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const iconTones: Record<string, string> = {
|
||||
gap: 0.85rem;
|
||||
align-items: flex-start;
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
border-radius: var(--radius);
|
||||
padding: 1.1rem 1.15rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
@@ -67,7 +67,7 @@ const iconTones: Record<string, string> = {
|
||||
.kpi-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
@@ -101,7 +101,7 @@ const cols = computed<Col[]>(() => {
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1.05rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
@@ -151,7 +151,7 @@ a {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.55rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f7;
|
||||
color: inherit;
|
||||
@@ -185,12 +185,12 @@ a {
|
||||
.deal-mark {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
background: #ecfdf8;
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ const rows = computed<Row[]>(() => {
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1.05rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
@@ -151,7 +151,7 @@ a {
|
||||
.act-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: #ecfdf8;
|
||||
color: #14b8a6;
|
||||
display: grid;
|
||||
|
||||
@@ -47,7 +47,7 @@ function trophy(rank: number) {
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1.05rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
@@ -89,12 +89,12 @@ a {
|
||||
.cust-avatar {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(145deg, #ccfbf1, #99f6e4);
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -775,7 +775,7 @@ onMounted(async () => {
|
||||
gap: 0.28rem;
|
||||
min-width: 0;
|
||||
padding: 0.8rem 0.9rem;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
@@ -789,7 +789,7 @@ onMounted(async () => {
|
||||
align-items: baseline;
|
||||
gap: 0.3rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.balance-box em {
|
||||
@@ -843,7 +843,7 @@ onMounted(async () => {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.45rem 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -918,7 +918,7 @@ onMounted(async () => {
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 0.4rem;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -942,21 +942,21 @@ onMounted(async () => {
|
||||
color: #92400e;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.55rem 0.7rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
input,
|
||||
select {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.45rem 0.6rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.btn {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.45rem 0.8rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
|
||||
@@ -119,7 +119,7 @@ withDefaults(
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: #1e2937;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ function onSelect(key: string) {
|
||||
height: 38px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #334155;
|
||||
@@ -175,7 +175,7 @@ function onSelect(key: string) {
|
||||
list-style: none;
|
||||
width: 220px;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -193,7 +193,7 @@ function onSelect(key: string) {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius);
|
||||
background: transparent;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
|
||||
@@ -146,7 +146,7 @@ const colors = computed(() => PIPELINE_ACCENT[props.accent]);
|
||||
background: var(--badge-bg);
|
||||
color: var(--badge-fg);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ const colors = computed(() => PIPELINE_ACCENT[props.accent]);
|
||||
.titles h3 {
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
@@ -230,7 +230,7 @@ const colors = computed(() => PIPELINE_ACCENT[props.accent]);
|
||||
|
||||
.amount strong {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.01em;
|
||||
max-width: 100%;
|
||||
|
||||
@@ -295,10 +295,10 @@ onBeforeUnmount(() => {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.column-head h2 {
|
||||
@@ -315,7 +315,7 @@ onBeforeUnmount(() => {
|
||||
height: 1.7rem;
|
||||
padding: 0 0.4rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ onBeforeUnmount(() => {
|
||||
color: var(--ink);
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.col-menu button:hover:not(:disabled) {
|
||||
|
||||
@@ -233,7 +233,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.titles {
|
||||
@@ -244,7 +244,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
||||
|
||||
.titles strong {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -285,7 +285,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
||||
}
|
||||
|
||||
.amount strong {
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
padding: 0.2rem;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.95rem 1rem;
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
|
||||
@@ -133,7 +133,7 @@ function onPick(value: string) {
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'neutral' | 'accent' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||
size?: 'sm' | 'md';
|
||||
dot?: boolean;
|
||||
}>(),
|
||||
{
|
||||
variant: 'neutral',
|
||||
size: 'md',
|
||||
dot: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="ui-badge" :class="[variant, size]">
|
||||
<span v-if="dot" class="badge-dot" aria-hidden="true" />
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: var(--fw-semibold);
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ui-badge.sm {
|
||||
padding: 0.1rem 0.45rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* Variants */
|
||||
.ui-badge.neutral {
|
||||
background: var(--surface-subtle);
|
||||
color: var(--ink-secondary);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.ui-badge.accent,
|
||||
.ui-badge.primary {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent-deep);
|
||||
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
}
|
||||
|
||||
.ui-badge.success {
|
||||
background: var(--success-soft);
|
||||
color: var(--success-deep);
|
||||
border-color: color-mix(in srgb, var(--success) 30%, transparent);
|
||||
}
|
||||
|
||||
.ui-badge.warning {
|
||||
background: var(--warning-soft);
|
||||
color: var(--warning-deep);
|
||||
border-color: color-mix(in srgb, var(--warning) 30%, transparent);
|
||||
}
|
||||
|
||||
.ui-badge.danger {
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
border-color: color-mix(in srgb, var(--danger) 30%, transparent);
|
||||
}
|
||||
|
||||
.ui-badge.info {
|
||||
background: var(--info-soft);
|
||||
color: var(--info-deep);
|
||||
border-color: color-mix(in srgb, var(--info) 30%, transparent);
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'primary' | 'ghost' | 'dashed' | 'icon';
|
||||
type?: 'button' | 'submit';
|
||||
variant?: 'primary' | 'ghost' | 'dashed' | 'icon' | 'danger' | 'outline' | 'link' | 'secondary' | 'success';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
block?: boolean;
|
||||
}>(),
|
||||
{ variant: 'primary', type: 'button' },
|
||||
{
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
type: 'button',
|
||||
disabled: false,
|
||||
loading: false,
|
||||
block: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -14,9 +23,12 @@ withDefaults(
|
||||
<button
|
||||
:type="type"
|
||||
class="ui-btn"
|
||||
:class="[variant, { block }]"
|
||||
:disabled="disabled"
|
||||
:class="[variant, size, { block, loading }]"
|
||||
:disabled="disabled || loading"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<span v-if="loading" class="spinner" aria-hidden="true"></span>
|
||||
<slot name="icon" />
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
@@ -26,18 +38,52 @@ withDefaults(
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.95rem;
|
||||
gap: var(--sp-2);
|
||||
min-height: var(--control-h-md);
|
||||
height: var(--control-h-md);
|
||||
padding: 0 0.85rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
color: var(--accent-contrast);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
font-size: 0.88rem;
|
||||
font-weight: var(--fw-semibold);
|
||||
font-size: var(--fs-sm);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast), opacity var(--transition-fast);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.ui-btn.sm {
|
||||
min-height: var(--control-h-sm);
|
||||
height: var(--control-h-sm);
|
||||
padding: 0 0.65rem;
|
||||
font-size: var(--fs-xs);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.ui-btn.md {
|
||||
min-height: var(--control-h-md);
|
||||
height: var(--control-h-md);
|
||||
padding: 0 0.85rem;
|
||||
font-size: var(--fs-sm);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.ui-btn.lg {
|
||||
min-height: var(--control-h-lg);
|
||||
height: var(--control-h-lg);
|
||||
padding: 0 1.15rem;
|
||||
font-size: var(--fs-base);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.ui-btn:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.ui-btn:disabled {
|
||||
@@ -49,28 +95,125 @@ withDefaults(
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Variants */
|
||||
.ui-btn.ghost {
|
||||
background: #fff;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.ui-btn.ghost:hover:not(:disabled) {
|
||||
background: var(--surface-subtle);
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.ui-btn.outline {
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ui-btn.outline:hover:not(:disabled) {
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.ui-btn.secondary {
|
||||
background: var(--surface-subtle);
|
||||
color: var(--ink-secondary);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.ui-btn.secondary:hover:not(:disabled) {
|
||||
background: var(--line);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.ui-btn.danger {
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.ui-btn.danger:hover:not(:disabled) {
|
||||
background: var(--danger-hover);
|
||||
}
|
||||
|
||||
.ui-btn.success {
|
||||
background: var(--success);
|
||||
color: #fff;
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.ui-btn.success:hover:not(:disabled) {
|
||||
background: var(--success-deep);
|
||||
}
|
||||
|
||||
.ui-btn.dashed {
|
||||
background: #fff;
|
||||
border: 1.5px dashed currentColor;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
border: 1.5px dashed var(--line-strong);
|
||||
}
|
||||
|
||||
.ui-btn.dashed:hover:not(:disabled) {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.ui-btn.link {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--accent);
|
||||
padding: 0 var(--sp-1);
|
||||
min-height: auto;
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
|
||||
.ui-btn.link:hover:not(:disabled) {
|
||||
background: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ui-btn.icon {
|
||||
width: var(--control-h);
|
||||
width: var(--control-h-md);
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
color: var(--accent-deep);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ui-btn.icon.sm {
|
||||
width: var(--control-h-sm);
|
||||
min-height: var(--control-h-sm);
|
||||
}
|
||||
|
||||
.ui-btn.icon.lg {
|
||||
width: var(--control-h-lg);
|
||||
min-height: var(--control-h-lg);
|
||||
}
|
||||
|
||||
.ui-btn.icon:hover:not(:disabled) {
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.ui-btn:focus-visible {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: ui-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ui-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
shadow?: 'none' | 'xs' | 'sm' | 'md';
|
||||
interactive?: boolean;
|
||||
title?: string;
|
||||
}>(),
|
||||
{
|
||||
padding: 'md',
|
||||
shadow: 'xs',
|
||||
interactive: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui-card">
|
||||
<slot />
|
||||
<div
|
||||
class="ui-card"
|
||||
:class="[
|
||||
`pad-${padding}`,
|
||||
`shadow-${shadow}`,
|
||||
{ interactive }
|
||||
]"
|
||||
>
|
||||
<header v-if="title || $slots.header" class="card-head">
|
||||
<slot name="header">
|
||||
<h3 v-if="title" class="card-title">{{ title }}</h3>
|
||||
</slot>
|
||||
<div v-if="$slots.actions" class="card-actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-body">
|
||||
<slot />
|
||||
</div>
|
||||
<footer v-if="$slots.footer" class="card-foot">
|
||||
<slot name="footer" />
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-card {
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--pad-modal);
|
||||
box-sizing: border-box;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
/* Padding variants */
|
||||
.ui-card.pad-none {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-card.pad-sm {
|
||||
padding: var(--sp-3);
|
||||
}
|
||||
|
||||
.ui-card.pad-md {
|
||||
padding: var(--sp-5);
|
||||
}
|
||||
|
||||
.ui-card.pad-lg {
|
||||
padding: var(--sp-6);
|
||||
}
|
||||
|
||||
/* Shadow variants */
|
||||
.ui-card.shadow-none {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ui-card.shadow-xs {
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.ui-card.shadow-sm {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.ui-card.shadow-md {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.ui-card.interactive:hover {
|
||||
border-color: var(--line-strong);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
padding-bottom: var(--sp-3);
|
||||
margin-bottom: var(--sp-4);
|
||||
border-bottom: 1px solid var(--line-light);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-md);
|
||||
font-weight: var(--fw-bold);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
padding-top: var(--sp-3);
|
||||
margin-top: var(--sp-4);
|
||||
border-top: 1px solid var(--line-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,54 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { LineChart, PieChart } from 'echarts/charts';
|
||||
import { LineChart, PieChart, BarChart } from 'echarts/charts';
|
||||
import {
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent,
|
||||
TitleComponent,
|
||||
} from 'echarts/components';
|
||||
import VChart from 'vue-echarts';
|
||||
import type { ComposeOption } from 'echarts/core';
|
||||
import type { LineSeriesOption, PieSeriesOption } from 'echarts/charts';
|
||||
import type { LineSeriesOption, PieSeriesOption, BarSeriesOption } from 'echarts/charts';
|
||||
import type {
|
||||
GridComponentOption,
|
||||
LegendComponentOption,
|
||||
TooltipComponentOption,
|
||||
TitleComponentOption,
|
||||
} from 'echarts/components';
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
LineChart,
|
||||
PieChart,
|
||||
BarChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
]);
|
||||
|
||||
export type UiChartOption = ComposeOption<
|
||||
| LineSeriesOption
|
||||
| PieSeriesOption
|
||||
| BarSeriesOption
|
||||
| GridComponentOption
|
||||
| TooltipComponentOption
|
||||
| LegendComponentOption
|
||||
| TitleComponentOption
|
||||
>;
|
||||
|
||||
/**
|
||||
* Shared palette matching Montact CRM design tokens
|
||||
*/
|
||||
export const CHART_PALETTE = [
|
||||
'#14b8a6', // Teal (Accent)
|
||||
'#3b82f6', // Blue
|
||||
'#f59e0b', // Amber
|
||||
'#10b981', // Emerald
|
||||
'#8b5cf6', // Violet
|
||||
'#f43f5e', // Rose
|
||||
'#06b6d4', // Cyan
|
||||
'#eab308', // Yellow
|
||||
];
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import VChart from 'vue-echarts';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
option: UiChartOption;
|
||||
height?: string;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{ height: '220px' },
|
||||
{ height: '260px', loading: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VChart class="ui-chart" :option="option" :style="{ height }" autoresize />
|
||||
<div class="ui-chart-container" :style="{ height }">
|
||||
<div v-if="loading" class="chart-loading">
|
||||
<div class="chart-spinner"></div>
|
||||
</div>
|
||||
<VChart v-else class="ui-chart" :option="option" autoresize />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-chart {
|
||||
.ui-chart-container {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chart-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.chart-spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 3px solid var(--line);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: chart-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes chart-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
type IconName = InstanceType<typeof UiIcon>['$props']['name'];
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: IconName;
|
||||
}>(),
|
||||
{
|
||||
title: 'موردی یافت نشد',
|
||||
icon: 'search',
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui-empty-state">
|
||||
<div class="icon-wrap">
|
||||
<slot name="icon">
|
||||
<UiIcon :name="icon" :size="32" />
|
||||
</slot>
|
||||
</div>
|
||||
<h3 class="title">{{ title }}</h3>
|
||||
<p v-if="description" class="description">{{ description }}</p>
|
||||
<slot />
|
||||
<div v-if="$slots.action" class="action-wrap">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: var(--sp-10) var(--sp-6);
|
||||
gap: var(--sp-3);
|
||||
color: var(--muted);
|
||||
background: var(--surface);
|
||||
border: 1px dashed var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-subtle);
|
||||
color: var(--muted);
|
||||
margin-bottom: var(--sp-1);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-md);
|
||||
font-weight: var(--fw-bold);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
font-size: var(--fs-sm);
|
||||
max-width: 360px;
|
||||
line-height: var(--lh-relaxed);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.action-wrap {
|
||||
margin-top: var(--sp-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
forId?: string;
|
||||
required?: boolean;
|
||||
hint?: string;
|
||||
error?: string | boolean;
|
||||
horizontal?: boolean;
|
||||
}>(),
|
||||
{
|
||||
required: false,
|
||||
horizontal: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui-field" :class="{ horizontal, hasError: !!error }">
|
||||
<div v-if="label || $slots.label" class="label-row">
|
||||
<label v-if="label" :for="forId" class="field-label">
|
||||
{{ label }}
|
||||
<span v-if="required" class="required-mark" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<slot v-else name="label" />
|
||||
<slot name="label-extra" />
|
||||
</div>
|
||||
|
||||
<div class="control-wrap">
|
||||
<slot />
|
||||
<p v-if="typeof error === 'string' && error" class="field-error" role="alert">
|
||||
{{ error }}
|
||||
</p>
|
||||
<p v-else-if="hint || $slots.hint" class="field-hint">
|
||||
<slot name="hint">{{ hint }}</slot>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-1);
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-field.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
.ui-field.horizontal .label-row {
|
||||
flex: 0 0 140px;
|
||||
}
|
||||
|
||||
.ui-field.horizontal .control-wrap {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: var(--fw-semibold);
|
||||
color: var(--ink-secondary);
|
||||
line-height: var(--lh-tight);
|
||||
}
|
||||
|
||||
.required-mark {
|
||||
color: var(--danger);
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.control-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
margin: 2px 0 0;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--danger);
|
||||
font-weight: var(--fw-medium);
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
margin: 2px 0 0;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--muted);
|
||||
line-height: var(--lh-normal);
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,7 @@ const open = ref(false);
|
||||
<div class="row">
|
||||
<slot name="before" />
|
||||
<UiSearch v-model="search" :placeholder="placeholder" class="search" />
|
||||
<UiButton v-if="showFilters" type="button" @click="open = !open">
|
||||
<UiButton v-if="showFilters" variant="secondary" type="button" @click="open = !open">
|
||||
<UiIcon name="filter" :size="16" />
|
||||
فیلترها
|
||||
</UiButton>
|
||||
@@ -54,9 +54,10 @@ const open = ref(false);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
gap: 0.65rem;
|
||||
padding: var(--pad-modal);
|
||||
padding: var(--sp-4);
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,48 +4,122 @@ withDefaults(
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
readonly?: boolean;
|
||||
required?: boolean;
|
||||
maxlength?: number;
|
||||
minlength?: number;
|
||||
error?: boolean | string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
maxlength?: number | string;
|
||||
minlength?: number | string;
|
||||
id?: string;
|
||||
autocomplete?: string;
|
||||
}>(),
|
||||
{ type: 'text' },
|
||||
{
|
||||
type: 'text',
|
||||
size: 'md',
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
required: false,
|
||||
},
|
||||
);
|
||||
|
||||
const model = defineModel<string>({ default: '' });
|
||||
const model = defineModel<string | number>({ default: '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="model"
|
||||
class="ui-input"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:maxlength="maxlength"
|
||||
:minlength="minlength"
|
||||
/>
|
||||
<div class="ui-input-wrapper" :class="[size, { disabled, hasError: !!error }]">
|
||||
<slot name="prefix" />
|
||||
<input
|
||||
:id="id"
|
||||
v-model="model"
|
||||
class="ui-input"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:required="required"
|
||||
:maxlength="maxlength"
|
||||
:minlength="minlength"
|
||||
:autocomplete="autocomplete"
|
||||
:aria-invalid="!!error"
|
||||
/>
|
||||
<slot name="suffix" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-input {
|
||||
.ui-input-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.85rem;
|
||||
min-height: var(--control-h-md);
|
||||
padding: 0 var(--sp-3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ui-input:focus {
|
||||
.ui-input-wrapper.sm {
|
||||
min-height: var(--control-h-sm);
|
||||
padding: 0 var(--sp-2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.ui-input-wrapper.md {
|
||||
min-height: var(--control-h-md);
|
||||
padding: 0 var(--sp-3);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--fs-base);
|
||||
}
|
||||
|
||||
.ui-input-wrapper.lg {
|
||||
min-height: var(--control-h-lg);
|
||||
padding: 0 var(--sp-4);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--fs-md);
|
||||
}
|
||||
|
||||
.ui-input {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-input::placeholder {
|
||||
color: var(--muted-light);
|
||||
}
|
||||
|
||||
.ui-input-wrapper:focus-within {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ui-input-wrapper.hasError {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.ui-input-wrapper.hasError:focus-within {
|
||||
outline: 2px solid var(--danger-soft);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.ui-input-wrapper.disabled {
|
||||
background: var(--surface-subtle);
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ui-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,7 +61,7 @@ function onBackdrop(event: MouseEvent) {
|
||||
z-index: 80;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1.25rem;
|
||||
padding: 16px;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
@@ -72,55 +72,122 @@ function onBackdrop(event: MouseEvent) {
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius, 10px);
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.16);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.head,
|
||||
.body,
|
||||
.foot {
|
||||
padding: var(--pad-modal);
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding-bottom: 0;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.close {
|
||||
width: var(--control-h);
|
||||
height: var(--control-h);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius, 10px);
|
||||
background: #fff;
|
||||
color: var(--muted);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
background: var(--surface-subtle);
|
||||
color: var(--ink);
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: auto;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding-top: 1rem;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.body :deep(.crm-page.embedded),
|
||||
.body :deep(.page.embedded),
|
||||
.body :deep(.product-form-page.embedded),
|
||||
.body :deep(.company-form-page.embedded) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.body :deep(.page.embedded > .form),
|
||||
.body :deep(.page.embedded > form),
|
||||
.body :deep(.crm-page.embedded > .form),
|
||||
.body :deep(.crm-page.embedded > form),
|
||||
.body :deep(.crm-page.embedded > .crm-form) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.body :deep(.embed-actions),
|
||||
.body :deep(.embedded-actions),
|
||||
.body :deep(.form-bottom-actions),
|
||||
.body :deep(.form-footer-actions) {
|
||||
order: 99;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
bottom: -16px;
|
||||
margin-top: auto;
|
||||
margin-inline: -16px;
|
||||
margin-bottom: -16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--line);
|
||||
z-index: 10;
|
||||
box-shadow: 0 -4px 14px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.body :deep(.embed-actions .btn),
|
||||
.body :deep(.embedded-actions .btn),
|
||||
.body :deep(.embed-actions .ui-btn),
|
||||
.body :deep(.embedded-actions .ui-btn),
|
||||
.body :deep(.form-bottom-actions .btn),
|
||||
.body :deep(.form-bottom-actions .ui-btn),
|
||||
.body :deep(.form-footer-actions .btn),
|
||||
.body :deep(.form-footer-actions .ui-btn) {
|
||||
border-radius: var(--radius, 10px);
|
||||
}
|
||||
|
||||
.foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding-top: 0;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--line);
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -138,62 +205,15 @@ function onBackdrop(event: MouseEvent) {
|
||||
}
|
||||
|
||||
.head {
|
||||
flex-shrink: 0;
|
||||
padding: 0.75rem 1rem 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.body:has(.page.embedded) {
|
||||
overflow: hidden;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.body :deep(.page.embedded) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.body :deep(.page.embedded > .form),
|
||||
.body :deep(.page.embedded > form) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.body :deep(.embed-actions) {
|
||||
order: 20;
|
||||
flex: 0 0 auto;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
margin-inline: -1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.body :deep(.embed-actions .btn) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.foot {
|
||||
flex-shrink: 0;
|
||||
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom));
|
||||
padding: 16px calc(16px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
lead?: string;
|
||||
description?: string;
|
||||
backTo?: string | object;
|
||||
}>(),
|
||||
{},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="ui-page-header">
|
||||
<div class="header-main">
|
||||
<RouterLink v-if="backTo" :to="backTo" class="back-link" title="بازگشت">
|
||||
<UiIcon name="arrow-right" :size="18" />
|
||||
</RouterLink>
|
||||
|
||||
<div class="titles">
|
||||
<h1 v-if="title" class="title">{{ title }}</h1>
|
||||
<slot v-else name="title" />
|
||||
|
||||
<p v-if="lead || description" class="lead">{{ lead || description }}</p>
|
||||
<slot v-else name="lead" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.actions" class="actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-4);
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--control-h-md);
|
||||
height: var(--control-h-md);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
color: var(--ink-secondary);
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
background: var(--surface-subtle);
|
||||
border-color: var(--line-strong);
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
|
||||
.titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-2xl);
|
||||
font-weight: var(--fw-extrabold);
|
||||
color: var(--ink);
|
||||
line-height: var(--lh-tight);
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 0;
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--muted);
|
||||
line-height: var(--lh-normal);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ui-page-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--fs-xl);
|
||||
}
|
||||
|
||||
.actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.actions > * {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import UiButton from './UiButton.vue';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
page: number;
|
||||
totalPages?: number;
|
||||
totalItems?: number;
|
||||
pageSize?: number;
|
||||
}>(),
|
||||
{
|
||||
page: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:page': [page: number];
|
||||
change: [page: number];
|
||||
}>();
|
||||
|
||||
const effectiveTotalPages = computed(() => {
|
||||
if (props.totalPages != null) return Math.max(1, props.totalPages);
|
||||
if (props.totalItems != null && props.pageSize != null && props.pageSize > 0) {
|
||||
return Math.max(1, Math.ceil(props.totalItems / props.pageSize));
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
|
||||
function goTo(p: number) {
|
||||
if (p < 1 || p > effectiveTotalPages.value || p === props.page) return;
|
||||
emit('update:page', p);
|
||||
emit('change', p);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="effectiveTotalPages > 1 || totalItems !== undefined" class="ui-pagination" aria-label="صفحهبندی">
|
||||
<div class="pagination-info">
|
||||
<span v-if="totalItems !== undefined">
|
||||
مجموع: <strong>{{ totalItems }}</strong> مورد
|
||||
</span>
|
||||
<span v-if="effectiveTotalPages > 1" class="page-count">
|
||||
(صفحه {{ page }} از {{ effectiveTotalPages }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="effectiveTotalPages > 1" class="pagination-controls">
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="page <= 1"
|
||||
title="صفحه اول"
|
||||
@click="goTo(1)"
|
||||
>
|
||||
<UiIcon name="chevrons-right" :size="14" />
|
||||
</UiButton>
|
||||
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="page <= 1"
|
||||
title="صفحه قبل"
|
||||
@click="goTo(page - 1)"
|
||||
>
|
||||
<UiIcon name="chevron-right" :size="14" />
|
||||
قبلی
|
||||
</UiButton>
|
||||
|
||||
<span class="current-indicator">
|
||||
{{ page }} / {{ effectiveTotalPages }}
|
||||
</span>
|
||||
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="page >= effectiveTotalPages"
|
||||
title="صفحه بعد"
|
||||
@click="goTo(page + 1)"
|
||||
>
|
||||
بعدی
|
||||
<UiIcon name="chevron-left" :size="14" />
|
||||
</UiButton>
|
||||
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="page >= effectiveTotalPages"
|
||||
title="صفحه آخر"
|
||||
@click="goTo(effectiveTotalPages)"
|
||||
>
|
||||
<UiIcon name="chevrons-left" :size="14" />
|
||||
</UiButton>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-3);
|
||||
padding-top: var(--sp-4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.page-count {
|
||||
color: var(--muted-light);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
|
||||
.current-indicator {
|
||||
padding: 0 var(--sp-2);
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: var(--fw-bold);
|
||||
color: var(--ink-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ui-pagination {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -29,12 +29,15 @@ const model = defineModel<string>({ default: '' });
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
height: var(--control-h);
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.75rem;
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
color: var(--muted);
|
||||
box-sizing: border-box;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.ui-search input {
|
||||
@@ -45,11 +48,14 @@ const model = defineModel<string>({ default: '' });
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: var(--fs-sm);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-search:focus-within {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||
}
|
||||
|
||||
.sr {
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
error?: boolean | string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
id?: string;
|
||||
required?: boolean;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
size: 'md',
|
||||
required: false,
|
||||
},
|
||||
);
|
||||
|
||||
const model = defineModel<string>({ default: '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="ui-select">
|
||||
<select v-model="model" :disabled="disabled">
|
||||
<label class="ui-select" :class="[size, { disabled, hasError: !!error }]">
|
||||
<select :id="id" v-model="model" :disabled="disabled" :required="required" :aria-invalid="!!error">
|
||||
<slot />
|
||||
</select>
|
||||
<UiIcon class="chev" name="chevron" :size="16" />
|
||||
<UiIcon class="chev" name="chevron" :size="size === 'sm' ? 14 : 16" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -22,20 +33,45 @@ const model = defineModel<string>({ default: '' });
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-select select {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 2rem 0 0.85rem;
|
||||
min-height: var(--control-h-md);
|
||||
padding: 0 2rem 0 var(--sp-3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-weight: var(--fw-medium);
|
||||
font-size: var(--fs-base);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.ui-select.sm select {
|
||||
min-height: var(--control-h-sm);
|
||||
padding: 0 1.75rem 0 var(--sp-2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.ui-select.md select {
|
||||
min-height: var(--control-h-md);
|
||||
padding: 0 2rem 0 var(--sp-3);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--fs-base);
|
||||
}
|
||||
|
||||
.ui-select.lg select {
|
||||
min-height: var(--control-h-lg);
|
||||
padding: 0 2.25rem 0 var(--sp-4);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--fs-md);
|
||||
}
|
||||
|
||||
.ui-select select:focus {
|
||||
@@ -43,6 +79,21 @@ const model = defineModel<string>({ default: '' });
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ui-select.hasError select {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.ui-select.hasError select:focus {
|
||||
outline: 2px solid var(--danger-soft);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.ui-select.disabled select {
|
||||
background: var(--surface-subtle);
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chev {
|
||||
position: absolute;
|
||||
left: 0.65rem;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
readonly?: boolean;
|
||||
required?: boolean;
|
||||
error?: boolean | string;
|
||||
rows?: number;
|
||||
maxlength?: number;
|
||||
id?: string;
|
||||
}>(),
|
||||
{
|
||||
rows: 3,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
required: false,
|
||||
},
|
||||
);
|
||||
|
||||
const model = defineModel<string>({ default: '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea
|
||||
:id="id"
|
||||
v-model="model"
|
||||
class="ui-textarea"
|
||||
:class="{ disabled, hasError: !!error }"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:required="required"
|
||||
:rows="rows"
|
||||
:maxlength="maxlength"
|
||||
:aria-invalid="!!error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-textarea {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
padding: var(--sp-3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: var(--fs-base);
|
||||
line-height: var(--lh-normal);
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.ui-textarea::placeholder {
|
||||
color: var(--muted-light);
|
||||
}
|
||||
|
||||
.ui-textarea:focus {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ui-textarea.hasError {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.ui-textarea.hasError:focus {
|
||||
outline: 2px solid var(--danger-soft);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.ui-textarea.disabled {
|
||||
background: var(--surface-subtle);
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
export { default as UiButton } from './UiButton.vue';
|
||||
export { default as UiInput } from './UiInput.vue';
|
||||
export { default as UiTextarea } from './UiTextarea.vue';
|
||||
export { default as UiSelect } from './UiSelect.vue';
|
||||
export { default as UiField } from './UiField.vue';
|
||||
export { default as UiCard } from './UiCard.vue';
|
||||
export { default as UiModal } from './UiModal.vue';
|
||||
export { default as UiPageHeader } from './UiPageHeader.vue';
|
||||
export { default as UiBadge } from './UiBadge.vue';
|
||||
export { default as UiPagination } from './UiPagination.vue';
|
||||
export { default as UiEmptyState } from './UiEmptyState.vue';
|
||||
export { default as UiSearch } from './UiSearch.vue';
|
||||
export { default as UiFilterBar } from './UiFilterBar.vue';
|
||||
export { default as UiSegmented } from './UiSegmented.vue';
|
||||
export { default as UiChart, CHART_PALETTE, type UiChartOption } from './UiChart.vue';
|
||||
Reference in New Issue
Block a user