feat(user-apps): appearance settings (dark mode + reduced motion), product morph, filter sheet
Applies to both user-app and user-app-v2. Logic and structure are shared; each app keeps its own look. Appearance settings - New "تنظیمات ظاهری" page (/settings/appearance), opened from the dashboard and available without login. It offers a light/dark/system theme and a "fast & light" motion switch. - appearanceStore saves the choice to localStorage. A small inline boot script in index.html applies it before first paint so there is no white flash. The "system" option follows OS changes live. - New ToggleSwitch component. Dark mode - Theme LESS tokens now compile to CSS custom properties. Light/dark values come from a generated virtual:palette.css (themeConfig/ appearance.ts, with colors.json + new colors.dark.json). - New LESS alpha() plugin replaces fade() on theme tokens. - New semantic tokens: page, onInk, heroSurface, hairlineStrong, photoMat, photoStage. - The dark palette is layered (page < card < panel < border) so surfaces separate without shadows. - Hard-coded colors were replaced by tokens according to their role. - antd inputs/text get dark overrides; vant uses its own .van-theme-dark. - Light mode looks the same as before. Reduced motion - With html.motion-reduced, transitions are capped at 120ms, keyframe loops stop, and backdrop blurs and permanent will-change are dropped. - Spinners and the toast progress bar (which closes the toast) are exempt via .keep-motion. - Carousel autoplay, the v1 Lottie and the product morph honor the setting. Product detail - Opening a product is now a container-transform morph from the tapped card (ProductMorph). It replaces the bottom Drawer and uses composited transform/opacity only. - The detail view is seeded with the card's data, so it no longer opens blank and jumps. Filters and sheets - Filters are now a full-width bottom sheet. Selections stay a draft until applied, so the result list no longer re-renders while dragging the slider. - DropDown collapse animates grid-template-rows instead of a capped max-height. - v2 IslandSheet is restyled to the v2 design language (API unchanged). Fixes - filtersStore.reset() shallow-copied shared arrays, so clearing the filters never cleared the selected brands.
This commit is contained in:
@@ -257,7 +257,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.cart {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ onMounted(async () => {
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ onMounted(async () => {
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ onMounted(async () => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
background: alpha(@canvas, 92%);
|
||||
backdrop-filter: blur(14px);
|
||||
border-top: 1px solid @hairline;
|
||||
padding-bottom: 66px;
|
||||
@@ -453,7 +453,7 @@ onMounted(async () => {
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
@@ -482,7 +482,7 @@ onMounted(async () => {
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -76,27 +76,15 @@
|
||||
</section>
|
||||
</container>
|
||||
</PullRefresh>
|
||||
|
||||
<Drawer
|
||||
v-model:visible="productDrawer"
|
||||
placement="bottom"
|
||||
:closable="false"
|
||||
class="rtl"
|
||||
height="100vh"
|
||||
:bodyStyle="{ padding: 0, position: 'relative' }"
|
||||
>
|
||||
<SingleProduct v-if="activeProductId" :id="activeProductId" @closeDrawer="productDrawer = false" />
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import Icon from '../components/icon/Icon.vue'
|
||||
import SingleProduct from '../components/product/SingleProduct.vue'
|
||||
import { openProduct as openProductMorph } from '../composables/useProductMorph'
|
||||
import RateStrip from '../components/home/RateStrip.vue'
|
||||
import HeroCarousel from '../components/home/HeroCarousel.vue'
|
||||
import type { HeroSlide } from '../components/home/HeroCarousel.vue'
|
||||
@@ -128,8 +116,6 @@ const goldPrice = ref<GoldPriceQuote | null>(null)
|
||||
// wired up; without it, slides configured in the admin panel never showed here.
|
||||
const adminSlides = ref<AdminSlide[] | null>(null)
|
||||
|
||||
const productDrawer = ref(false)
|
||||
const activeProductId = ref<string | null>(null)
|
||||
|
||||
// First-paint entrance. Flipped once, after the first data load, so the
|
||||
// sections settle into place instead of appearing all at once; pull-to-refresh
|
||||
@@ -189,9 +175,10 @@ function productImageForCategory(categoryId: string): string | undefined {
|
||||
return match?.images?.[0]?.url
|
||||
}
|
||||
|
||||
// Opens as a morph out of the tapped card (ProductMorph.vue, mounted in
|
||||
// App.vue) — the card is found from the tap, so the emit chain stays as is.
|
||||
function openProduct(product: ProductSummary) {
|
||||
activeProductId.value = product.id
|
||||
productDrawer.value = true
|
||||
openProductMorph(product)
|
||||
}
|
||||
|
||||
function onCategory(cat: HomeCategory) {
|
||||
@@ -266,7 +253,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.home {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -276,13 +263,13 @@ onMounted(async () => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
background: alpha(@canvas, 82%);
|
||||
backdrop-filter: blur(18px) saturate(140%);
|
||||
// A hairline rule cut a hard line across the page. A very soft fade under
|
||||
// the bar separates it from the content that scrolls beneath without
|
||||
// drawing an edge.
|
||||
border-bottom: 1px solid fade(@primary, 5%);
|
||||
box-shadow: 0 8px 20px -18px fade(@primary, 55%);
|
||||
border-bottom: 1px solid alpha(@primary, 5%);
|
||||
box-shadow: 0 8px 20px -18px alpha(@primary, 55%);
|
||||
}
|
||||
|
||||
.home__barInner {
|
||||
|
||||
@@ -82,7 +82,7 @@ const pull = async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.newest {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ onBeforeUnmount(() => observer?.disconnect())
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
color: #06b6d4;
|
||||
background: #ecfeff;
|
||||
background: fade(#06b6d4, 12%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -419,7 +419,7 @@ onBeforeUnmount(() => observer?.disconnect())
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: @ink;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -252,13 +252,13 @@ onMounted(load)
|
||||
}
|
||||
|
||||
.cch__state.is-on {
|
||||
color: #16a34a;
|
||||
background: #dcfce7;
|
||||
color: @green;
|
||||
background: alpha(@green, 14%);
|
||||
}
|
||||
|
||||
.cch__state.is-off {
|
||||
color: #dc2626;
|
||||
background: #fee2e2;
|
||||
color: @red;
|
||||
background: alpha(@red, 12%);
|
||||
}
|
||||
|
||||
.cch__facts {
|
||||
@@ -350,7 +350,7 @@ onMounted(load)
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: @ink;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -189,10 +189,10 @@ function rial(value: string): string {
|
||||
}
|
||||
|
||||
.is-in {
|
||||
color: #16a34a;
|
||||
color: @green;
|
||||
}
|
||||
|
||||
.is-out {
|
||||
color: #dc2626;
|
||||
color: @red;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -366,11 +366,11 @@ onBeforeUnmount(() => observer?.disconnect())
|
||||
}
|
||||
|
||||
.is-in {
|
||||
color: #16a34a;
|
||||
color: @green;
|
||||
}
|
||||
|
||||
.is-out {
|
||||
color: #dc2626;
|
||||
color: @red;
|
||||
}
|
||||
|
||||
.rep__more,
|
||||
|
||||
@@ -67,7 +67,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.coll {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="search">
|
||||
<PageBar :title="showFilter ? 'فیلترها' : 'جستجو'" :back="showFilter" @click.capture="onBarClick">
|
||||
<PageBar title="جستجو">
|
||||
<template #below>
|
||||
<div class="search__barExtra">
|
||||
<SearchInput @search="search" place-holder="جستجوی دسته بندی، محصولات و ..." />
|
||||
|
||||
<div v-if="!showFilter" class="search__chips no-scrollbar">
|
||||
<div class="search__chips no-scrollbar">
|
||||
<button
|
||||
type="button"
|
||||
class="search__chip search__chip--filter"
|
||||
@@ -54,15 +54,7 @@
|
||||
</PageBar>
|
||||
|
||||
<Spinner :spinning="loadingPage" tip="درحال دریافت اطلاعات">
|
||||
<Filters
|
||||
v-model:filterKeys="filterKeys"
|
||||
v-show="showFilter"
|
||||
v-model="filters.query"
|
||||
:products="allProducts"
|
||||
@change="hideFilters"
|
||||
/>
|
||||
|
||||
<PullRefresh v-show="!showFilter" v-model="refreshing" @refresh="onRefresh">
|
||||
<PullRefresh v-model="refreshing" @refresh="onRefresh">
|
||||
<template #pulling><Loading size="18" /></template>
|
||||
<template #loosing><Loading size="18" /></template>
|
||||
<template #loading><Loading size="18" /></template>
|
||||
@@ -93,12 +85,20 @@
|
||||
</container>
|
||||
</PullRefresh>
|
||||
</Spinner>
|
||||
|
||||
<!-- A bottom sheet over a see-through backdrop (teleported to <body>);
|
||||
see Filters.vue. The result list stays in place underneath. -->
|
||||
<Filters
|
||||
v-model:show="showFilter"
|
||||
v-model:filterKeys="filterKeys"
|
||||
v-model="filters.query"
|
||||
:products="allProducts"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
@@ -108,11 +108,9 @@ import Filters from '../../components/search/Filters.vue'
|
||||
import SectionHead from '../../components/home/SectionHead.vue'
|
||||
import ProductCard from '../../components/product/ProductCard.vue'
|
||||
import { useFiltersStore } from '../../stores/filtersStore'
|
||||
import { useNavStore } from '../../stores/navStore'
|
||||
import { api, type ProductSummary } from '../../api/client'
|
||||
|
||||
const filters = useFiltersStore()
|
||||
const nav = useNavStore()
|
||||
|
||||
const loadingPage = ref(false)
|
||||
const refreshing = ref(false)
|
||||
@@ -211,50 +209,14 @@ function setCats(id: string | undefined) {
|
||||
}
|
||||
function showFilters() {
|
||||
showFilter.value = true
|
||||
nav.setNavVisibility(false)
|
||||
}
|
||||
function hideFilters(event?: { resetFilters: boolean }) {
|
||||
showFilter.value = false
|
||||
nav.setNavVisibility(true)
|
||||
if (event?.resetFilters) {
|
||||
filters.reset()
|
||||
filterKeys.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// While the filter sheet is open the bar's back button closes the sheet
|
||||
// rather than leaving the route, matching the legacy "بازگشت" affordance.
|
||||
function onBarClick(e: MouseEvent) {
|
||||
if (!showFilter.value) return
|
||||
const btn = (e.target as HTMLElement)?.closest('button')
|
||||
if (btn) {
|
||||
e.stopPropagation()
|
||||
hideFilters()
|
||||
}
|
||||
}
|
||||
|
||||
// The filter sheet deliberately hides the bottom tab bar while it is open, so
|
||||
// its own fixed action bar ("اعمال فیلتر" / "حذف فیلترها") stays clickable —
|
||||
// the tab bar sits at the same bottom edge with a higher z-index. But leaving
|
||||
// the page with the sheet still open (browser/swipe back, or tapping a
|
||||
// collection tile) never ran hideFilters, so that hide leaked: showNav stayed
|
||||
// false, and on any destination outside the five tab routes the user was left
|
||||
// with no tab bar at all until a full page refresh reset the store.
|
||||
//
|
||||
// Restoring it here rather than in onBeforeUnmount is deliberate: this guard
|
||||
// runs BEFORE router.afterEach, which then writes the correct visibility for
|
||||
// the destination and gets the last word. onBeforeUnmount runs after it, and
|
||||
// would wrongly re-show the bar on full-page routes like /product/:id.
|
||||
onBeforeRouteLeave(() => {
|
||||
if (showFilter.value) nav.setNavVisibility(true)
|
||||
})
|
||||
|
||||
onMounted(loadProducts)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.search {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -305,7 +267,7 @@ onMounted(loadProducts)
|
||||
}
|
||||
|
||||
.search__chip.is-on {
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
<!--
|
||||
تنظیمات ظاهری — the colour theme and the "fast & light" motion switch.
|
||||
Reached from the dashboard; open to signed-out users too (these are device
|
||||
preferences, not account data). State lives in stores/appearanceStore.ts,
|
||||
which applies it to <html> immediately — nothing to save or confirm.
|
||||
|
||||
The theme previews are drawn with FIXED colours, not theme tokens: each one
|
||||
has to show what that theme looks like, whichever theme is active now.
|
||||
|
||||
STRUCTURE IS SHARED, LOOK IS NOT: template and script are identical in
|
||||
user-app and user-app-v2; only the "Look" block differs, per app.
|
||||
-->
|
||||
<template>
|
||||
<div class="ap">
|
||||
<PageBar title="تنظیمات ظاهری" back-to="userPage::hasSubRoute" />
|
||||
|
||||
<container class="ap__body">
|
||||
<!-- ── Theme ───────────────────────────────────────────── -->
|
||||
<section class="ap__section">
|
||||
<header class="ap__head">
|
||||
<h2 id="ap-theme" class="ap__title">پوسته برنامه</h2>
|
||||
<p class="ap__sub">ظاهر روشن یا تیره، یا هماهنگ با تنظیمات گوشی شما</p>
|
||||
</header>
|
||||
|
||||
<div class="ap__themes" role="radiogroup" aria-labelledby="ap-theme">
|
||||
<button
|
||||
v-for="opt in THEMES"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
role="radio"
|
||||
class="ap__theme"
|
||||
:class="{ 'is-on': appearance.theme === opt.value }"
|
||||
:aria-checked="appearance.theme === opt.value"
|
||||
@click="appearance.setTheme(opt.value)"
|
||||
>
|
||||
<span class="ap__preview" :class="`ap__preview--${opt.value}`" aria-hidden="true">
|
||||
<span class="ap__pvBar"></span>
|
||||
<span class="ap__pvCard"></span>
|
||||
<span class="ap__pvLine"></span>
|
||||
<span class="ap__pvLine ap__pvLine--short"></span>
|
||||
<span class="ap__pvBtn"></span>
|
||||
</span>
|
||||
<span class="ap__themeLabel">
|
||||
<Icon :icon="opt.icon" size="15" />
|
||||
{{ opt.label }}
|
||||
</span>
|
||||
<span class="ap__check" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:tick-circle" size="18" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="appearance.theme === 'system'" class="ap__note">
|
||||
الان {{ appearance.resolvedTheme === 'dark' ? 'تیره' : 'روشن' }} است، مطابق تنظیمات گوشی.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ── Motion ──────────────────────────────────────────── -->
|
||||
<section class="ap__section">
|
||||
<header class="ap__head">
|
||||
<h2 class="ap__title">انیمیشنها</h2>
|
||||
<p class="ap__sub">برای سرعت بیشتر روی گوشیهای ضعیفتر</p>
|
||||
</header>
|
||||
|
||||
<!-- The whole row is the switch: one tap target, one announcement. -->
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
class="ap__row"
|
||||
:aria-checked="appearance.reduceMotion"
|
||||
@click="appearance.setReduceMotion(!appearance.reduceMotion)"
|
||||
>
|
||||
<span class="ap__rowIcon" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:flash" size="18" />
|
||||
</span>
|
||||
<span class="ap__rowText">
|
||||
<b>حالت سبک و سریع</b>
|
||||
<small>
|
||||
حرکتهای سنگین، افکتهای شیشهای و اسلاید خودکار خاموش میشوند؛ بازخورد کوتاه لمسها و جابهجاییهای سریع
|
||||
باقی میمانند تا برنامه خشک و بیروح نشود.
|
||||
</small>
|
||||
</span>
|
||||
<ToggleSwitch :on="appearance.reduceMotion" />
|
||||
</button>
|
||||
</section>
|
||||
</container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import ToggleSwitch from '../../components/ui/ToggleSwitch.vue'
|
||||
import { useAppearanceStore, type ThemeChoice } from '../../stores/appearanceStore'
|
||||
|
||||
const appearance = useAppearanceStore()
|
||||
|
||||
const THEMES: { value: ThemeChoice; label: string; icon: string }[] = [
|
||||
{ value: 'light', label: 'روشن', icon: 'vuesax-linear:sun-1' },
|
||||
{ value: 'dark', label: 'تیره', icon: 'vuesax-linear:moon' },
|
||||
{ value: 'system', label: 'مطابق سیستم', icon: 'vuesax-linear:monitor-mobbile' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* ── Structure (same in both apps) ──────────────────────────── */
|
||||
.ap {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.ap__body {
|
||||
padding-top: 76px;
|
||||
padding-bottom: 90px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 26px;
|
||||
}
|
||||
|
||||
.ap__title,
|
||||
.ap__sub,
|
||||
.ap__note {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ap__themes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ap__theme {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
.ap__theme:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.ap__themeLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ap__check {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
inset-inline-end: 8px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
opacity: 0;
|
||||
transform: scale(0.6);
|
||||
transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
.ap__theme.is-on .ap__check {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
// Mini screen, fixed colours on purpose (see the header).
|
||||
.ap__preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 4;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6%;
|
||||
padding: 10% 12%;
|
||||
--pv-bg: #ffffff;
|
||||
--pv-bar: #f4f5f6;
|
||||
--pv-card: #ecedef;
|
||||
--pv-line: #dadce0;
|
||||
--pv-btn: #14161a;
|
||||
background: var(--pv-bg);
|
||||
}
|
||||
.ap__preview--dark {
|
||||
--pv-bg: #16171a;
|
||||
--pv-bar: #1e2024;
|
||||
--pv-card: #2b2e34;
|
||||
--pv-line: #3a3e45;
|
||||
--pv-btn: #f1f2f4;
|
||||
}
|
||||
// System: the light screen with its lower-left half in dark.
|
||||
.ap__preview--system::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #16171a;
|
||||
clip-path: polygon(100% 0, 100% 100%, 0 100%);
|
||||
opacity: 0.94;
|
||||
}
|
||||
|
||||
.ap__pvBar {
|
||||
height: 9%;
|
||||
border-radius: 4px;
|
||||
background: var(--pv-bar);
|
||||
}
|
||||
.ap__pvCard {
|
||||
flex: 1;
|
||||
border-radius: 7px;
|
||||
background: var(--pv-card);
|
||||
}
|
||||
.ap__pvLine {
|
||||
height: 6%;
|
||||
border-radius: 3px;
|
||||
background: var(--pv-line);
|
||||
}
|
||||
.ap__pvLine--short {
|
||||
width: 60%;
|
||||
}
|
||||
.ap__pvBtn {
|
||||
height: 11%;
|
||||
border-radius: 999px;
|
||||
background: var(--pv-btn);
|
||||
}
|
||||
|
||||
.ap__row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-align: start;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: transform 0.15s ease, border-color 0.2s ease;
|
||||
}
|
||||
.ap__row:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.ap__rowIcon {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.ap__rowText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
/* ── Look: user-app-v2 ──────────────────────────────────────── */
|
||||
// Borderless @hairlineSoft panels at the 22px radius, chosen = an @ink ring
|
||||
// with a filled @ink check (the app's chips); the motion row is the
|
||||
// dashboard's own settings row, wine icon tile and all.
|
||||
.ap {
|
||||
background: @page;
|
||||
}
|
||||
|
||||
.ap__head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.ap__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
}
|
||||
.ap__sub {
|
||||
margin-top: 4px;
|
||||
font-size: 11.5px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
.ap__note {
|
||||
margin-top: 10px;
|
||||
font-size: 11.5px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.ap__theme {
|
||||
padding: 10px 10px 12px;
|
||||
border: 0;
|
||||
border-radius: 22px;
|
||||
background: @hairlineSoft;
|
||||
color: @inkMuted;
|
||||
box-shadow: inset 0 0 0 1.5px transparent;
|
||||
}
|
||||
.ap__theme.is-on {
|
||||
color: @ink;
|
||||
box-shadow: inset 0 0 0 1.5px @ink;
|
||||
}
|
||||
|
||||
.ap__preview {
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 0 0 1px rgba(20, 22, 26, 0.06);
|
||||
}
|
||||
|
||||
.ap__themeLabel {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ap__check {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
}
|
||||
|
||||
.ap__row {
|
||||
padding: 13px 14px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 17px;
|
||||
background: @canvas;
|
||||
}
|
||||
.ap__row:active {
|
||||
border-color: alpha(@wine, 20%);
|
||||
}
|
||||
|
||||
.ap__rowIcon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.ap__rowText b {
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
}
|
||||
.ap__rowText small {
|
||||
font-size: 10.5px;
|
||||
line-height: 1.9;
|
||||
color: @inkFaint;
|
||||
}
|
||||
</style>
|
||||
@@ -153,7 +153,7 @@ function finish() {
|
||||
.cpf__error {
|
||||
margin: 12px 2px 0;
|
||||
font-size: 12.5px;
|
||||
color: #dc2626;
|
||||
color: @red;
|
||||
}
|
||||
|
||||
.cpf__submit {
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<b>سفارشات</b>
|
||||
<small>پیگیری سفارشهای شما</small>
|
||||
</span>
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
||||
</button>
|
||||
|
||||
<button type="button" class="dash__row" @click="goToDetails(1)">
|
||||
@@ -127,7 +127,7 @@
|
||||
<b>اسناد</b>
|
||||
<small>تراکنشها و رسیدهای حساب</small>
|
||||
</span>
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
||||
</button>
|
||||
|
||||
<button type="button" class="dash__row" @click="goToDetails(2)">
|
||||
@@ -138,7 +138,7 @@
|
||||
<b>پروفایل</b>
|
||||
<small>اطلاعات و تنظیمات حساب</small>
|
||||
</span>
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
||||
</button>
|
||||
|
||||
<button type="button" class="dash__row" @click="goToDetails(3)">
|
||||
@@ -149,7 +149,7 @@
|
||||
<b>علاقمندی</b>
|
||||
<small>محصولات ذخیرهشده شما</small>
|
||||
</span>
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
||||
</button>
|
||||
<!--
|
||||
D24 — the account statement. The reference put it on the dashboard
|
||||
@@ -165,7 +165,20 @@
|
||||
<b>گزارش</b>
|
||||
<small>گردش حساب طلایی و ریالی شما</small>
|
||||
</span>
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
||||
</button>
|
||||
|
||||
<!-- Appearance (theme + reduced motion). Device-level preferences, not
|
||||
account data, so — unlike every row above — it needs no login. -->
|
||||
<button type="button" class="dash__row" @click="router.push({ name: 'appearanceSettings' })">
|
||||
<span class="dash__rowIcon" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:brush" size="18" />
|
||||
</span>
|
||||
<span class="dash__rowText">
|
||||
<b>تنظیمات ظاهری</b>
|
||||
<small>پوسته روشن و تیره، انیمیشنها</small>
|
||||
</span>
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
||||
</button>
|
||||
|
||||
<!--
|
||||
@@ -184,7 +197,7 @@
|
||||
<b>سفارش مشتریان</b>
|
||||
<small>سفارشهای مشتریان معرفیشده شما</small>
|
||||
</span>
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
||||
</button>
|
||||
|
||||
<button type="button" class="dash__row" @click="goToDetails(5)">
|
||||
@@ -195,7 +208,7 @@
|
||||
<b>مشتریان من</b>
|
||||
<small>فهرست مشتریان معرفیشده شما</small>
|
||||
</span>
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="#8A909C" />
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="16" color="var(--c-inkFaint)" />
|
||||
</button>
|
||||
</template>
|
||||
</section>
|
||||
@@ -315,7 +328,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.dash {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -327,10 +340,10 @@ onMounted(async () => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
background: alpha(@canvas, 82%);
|
||||
backdrop-filter: blur(18px) saturate(140%);
|
||||
border-bottom: 1px solid fade(@primary, 5%);
|
||||
box-shadow: 0 8px 20px -18px fade(@primary, 55%);
|
||||
border-bottom: 1px solid alpha(@primary, 5%);
|
||||
box-shadow: 0 8px 20px -18px alpha(@primary, 55%);
|
||||
}
|
||||
|
||||
.dash__barInner {
|
||||
@@ -404,7 +417,7 @@ onMounted(async () => {
|
||||
margin-bottom: 28px;
|
||||
padding: 20px 18px 16px;
|
||||
border-radius: 26px;
|
||||
background: linear-gradient(155deg, @ink 0%, @ink 55%, @wineDeep 130%);
|
||||
background: linear-gradient(155deg, @heroSurface 0%, @heroSurface 55%, #5E1427 130%);
|
||||
color: #fff;
|
||||
box-shadow: 0 22px 40px -26px rgba(20, 22, 26, 0.32);
|
||||
}
|
||||
@@ -447,7 +460,7 @@ onMounted(async () => {
|
||||
background: @wineSoft;
|
||||
}
|
||||
.dash__heroSeal.is-creditor {
|
||||
color: @ink;
|
||||
color: #14161A;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
@@ -500,7 +513,7 @@ onMounted(async () => {
|
||||
margin-top: 16px;
|
||||
padding: 26px 20px;
|
||||
border-radius: 26px;
|
||||
background: @ink;
|
||||
background: @heroSurface;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -540,7 +553,7 @@ onMounted(async () => {
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
color: #14161A;
|
||||
background: #fff;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
@@ -571,7 +584,7 @@ onMounted(async () => {
|
||||
}
|
||||
.dash__row:active {
|
||||
transform: scale(0.985);
|
||||
border-color: fade(@wine, 20%);
|
||||
border-color: alpha(@wine, 20%);
|
||||
}
|
||||
|
||||
.dash__rowIcon {
|
||||
|
||||
@@ -64,7 +64,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.favs {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ onMounted(async () => {
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -204,7 +204,7 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
|
||||
|
||||
<style scoped lang="less">
|
||||
.auth {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import PageBar from '../../components/ui/PageBar.vue'
|
||||
|
||||
<style scoped lang="less">
|
||||
.notif {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ onMounted(loadOrders)
|
||||
|
||||
<style scoped lang="less">
|
||||
.ord {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ onMounted(loadOrders)
|
||||
}
|
||||
|
||||
.ord__chip.is-on {
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
}
|
||||
@@ -313,7 +313,7 @@ onMounted(loadOrders)
|
||||
gap: 12px;
|
||||
padding: 11px 16px;
|
||||
border-bottom: 1px solid @hairline;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
background: alpha(@canvas, 92%);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ onMounted(loadOrders)
|
||||
}
|
||||
|
||||
.detail__step.is-done .detail__stepDot {
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.prof {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ onMounted(async () => {
|
||||
|
||||
.prof__seal.is-ok {
|
||||
color: @green;
|
||||
background: #f0fdf4;
|
||||
background: alpha(@green, 10%);
|
||||
}
|
||||
|
||||
.prof__seal.is-pending {
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
<footer class="reg__footer">
|
||||
<div class="reg__footerInner">
|
||||
<button type="submit" class="reg__cta" :class="{ 'is-loading': loading }" :disabled="loading">
|
||||
<span v-if="loading" class="reg__spinner" aria-hidden="true" />
|
||||
<span v-if="loading" class="reg__spinner keep-motion" aria-hidden="true" />
|
||||
<span>{{ step === 1 ? 'ادامه' : loading ? 'در حال ثبت…' : 'ثبت درخواست' }}</span>
|
||||
<Icon v-if="step === 1" icon="vuesax-linear:arrow-left" size="18" />
|
||||
</button>
|
||||
@@ -248,7 +248,7 @@ async function onFinish() {
|
||||
<style scoped lang="less">
|
||||
.reg {
|
||||
min-height: 100vh;
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
}
|
||||
|
||||
.reg__body {
|
||||
@@ -407,12 +407,12 @@ async function onFinish() {
|
||||
}
|
||||
|
||||
:deep(.ant-input::placeholder) {
|
||||
color: #b4b8c0;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
:deep(.ant-input:hover),
|
||||
:deep(.ant-input-affix-wrapper:hover) {
|
||||
border-color: darken(@hairline, 8%);
|
||||
border-color: @hairlineStrong;
|
||||
}
|
||||
|
||||
:deep(.ant-input:focus),
|
||||
@@ -420,7 +420,7 @@ async function onFinish() {
|
||||
:deep(.ant-input-affix-wrapper-focused) {
|
||||
border-color: @wine;
|
||||
background: @canvas;
|
||||
box-shadow: 0 0 0 4px fade(@wine, 10%);
|
||||
box-shadow: 0 0 0 4px alpha(@wine, 10%);
|
||||
|
||||
.ant-input-prefix {
|
||||
color: @wine;
|
||||
@@ -435,12 +435,12 @@ async function onFinish() {
|
||||
:deep(.ant-form-item-has-error .ant-input),
|
||||
:deep(.ant-form-item-has-error .ant-input-affix-wrapper) {
|
||||
border-color: @red !important;
|
||||
background: fade(@red, 3%);
|
||||
background: alpha(@red, 3%);
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-has-error .ant-input:focus),
|
||||
:deep(.ant-form-item-has-error .ant-input-affix-wrapper-focused) {
|
||||
box-shadow: 0 0 0 4px fade(@red, 10%) !important;
|
||||
box-shadow: 0 0 0 4px alpha(@red, 10%) !important;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-has-error .ant-input-affix-wrapper .ant-input) {
|
||||
@@ -501,7 +501,7 @@ async function onFinish() {
|
||||
bottom: 0;
|
||||
z-index: 15;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
background: fade(@canvas, 86%);
|
||||
background: alpha(@canvas, 86%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
border-top: 1px solid @hairline;
|
||||
@@ -526,9 +526,9 @@ async function onFinish() {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
color: @onInk;
|
||||
background: @ink;
|
||||
box-shadow: 0 10px 22px -12px fade(@ink, 70%);
|
||||
box-shadow: 0 10px 22px -12px alpha(@ink, 70%);
|
||||
transition: transform 0.15s ease, opacity 0.2s ease;
|
||||
|
||||
&:active {
|
||||
@@ -609,7 +609,7 @@ async function onFinish() {
|
||||
height: 88px;
|
||||
border-radius: 26px;
|
||||
color: @green;
|
||||
background: fade(@green, 10%);
|
||||
background: alpha(@green, 10%);
|
||||
animation: reg-pop 0.6s cubic-bezier(0.2, 1.5, 0.4, 1) both;
|
||||
|
||||
:deep(svg) {
|
||||
|
||||
@@ -152,7 +152,7 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.tx {
|
||||
background: @canvas;
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ onMounted(async () => {
|
||||
|
||||
.tx__mark.is-in {
|
||||
color: @green;
|
||||
background: #f0fdf4;
|
||||
background: alpha(@green, 10%);
|
||||
}
|
||||
|
||||
.tx__mark.is-out {
|
||||
|
||||
Reference in New Issue
Block a user