From f441ca828908c5bee2d9c36510de3bd62caaf534 Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Sun, 4 Oct 2026 16:18:56 +0330 Subject: [PATCH] feat(user-apps): appearance settings (dark mode + reduced motion), product morph, filter sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/user-app-v2/index.html | 27 +- apps/user-app-v2/src/App.vue | 2 + apps/user-app-v2/src/assets/styles/style.less | 109 ++- apps/user-app-v2/src/components/Spinner.vue | 3 +- .../src/components/home/CategoryGrid.vue | 8 + .../src/components/home/HeroCarousel.vue | 6 +- .../src/components/home/ProductPanel.vue | 10 +- .../src/components/home/RateStrip.vue | 10 +- .../src/components/home/ShowcaseCard.vue | 2 +- .../src/components/product/CartItem.vue | 8 + .../src/components/product/OrderItemCard.vue | 8 + .../src/components/product/ProductCard.vue | 33 +- .../src/components/product/ProductMorph.vue | 541 +++++++++++++++ .../src/components/product/SingleProduct.vue | 27 +- .../src/components/search/Filters.vue | 653 +++++++++++++----- .../src/components/ui/DropDown.vue | 155 +++-- .../src/components/ui/IslandSheet.vue | 319 +++------ .../src/components/ui/Navigation.vue | 26 +- .../user-app-v2/src/components/ui/PageBar.vue | 2 +- .../src/components/ui/ToastCard.vue | 22 +- .../src/components/ui/ToggleSwitch.vue | 63 ++ .../components/upload/SingleImageUploader.vue | 8 +- .../src/composables/useProductDetail.ts | 30 +- .../src/composables/useProductMorph.ts | 43 ++ apps/user-app-v2/src/main.ts | 7 + apps/user-app-v2/src/router/index.ts | 2 + .../user-app-v2/src/stores/appearanceStore.ts | 120 ++++ apps/user-app-v2/src/stores/filtersStore.ts | 13 +- apps/user-app-v2/src/utils/orderStatus.ts | 15 +- apps/user-app-v2/src/views/CartView.vue | 12 +- apps/user-app-v2/src/views/HomeView.vue | 29 +- apps/user-app-v2/src/views/NewestView.vue | 2 +- .../src/views/expert/ChildOrdersView.vue | 4 +- .../src/views/expert/CustomerChoiceView.vue | 10 +- .../src/views/report/ReportDocument.vue | 4 +- .../src/views/report/ReportView.vue | 4 +- .../src/views/search/CollectionView.vue | 2 +- .../src/views/search/SearchView.vue | 66 +- .../src/views/user/AppearanceView.vue | 341 +++++++++ .../src/views/user/CompleteProfileView.vue | 2 +- .../src/views/user/DashboardView.vue | 45 +- .../src/views/user/FavoritesView.vue | 4 +- apps/user-app-v2/src/views/user/LoginView.vue | 4 +- .../src/views/user/NotificationsView.vue | 2 +- .../user-app-v2/src/views/user/OrdersView.vue | 8 +- .../src/views/user/ProfileView.vue | 4 +- .../src/views/user/RegisterView.vue | 22 +- .../src/views/user/TransactionsView.vue | 4 +- apps/user-app-v2/themeConfig/appearance.ts | 154 +++++ apps/user-app-v2/themeConfig/colors.dark.json | 31 + apps/user-app-v2/themeConfig/themeConfig.ts | 12 +- apps/user-app-v2/vite.config.ts | 9 +- apps/user-app/index.html | 27 +- apps/user-app/src/App.vue | 11 +- apps/user-app/src/assets/styles/style.less | 110 ++- apps/user-app/src/components/DialogVant.vue | 6 +- .../src/components/DialogVantOneButton.vue | 6 +- apps/user-app/src/components/Spinner.vue | 3 +- .../src/components/home/CategoryRail.vue | 8 + .../src/components/home/ShowcaseCard.vue | 12 +- .../src/components/home/ShowcaseSlider.vue | 6 +- .../src/components/product/CartItem.vue | 8 + .../src/components/product/OrderItemCard.vue | 8 + .../src/components/product/ProductCard.vue | 31 +- .../src/components/product/ProductMorph.vue | 541 +++++++++++++++ .../src/components/product/SingleProduct.vue | 14 +- .../src/components/search/Filters.vue | 622 ++++++++++++----- apps/user-app/src/components/ui/DropDown.vue | 154 +++-- apps/user-app/src/components/ui/PageBar.vue | 2 +- apps/user-app/src/components/ui/ToastCard.vue | 22 +- .../src/components/ui/ToggleSwitch.vue | 59 ++ .../components/upload/SingleImageUploader.vue | 8 +- .../src/composables/useProductDetail.ts | 30 +- .../src/composables/useProductMorph.ts | 43 ++ apps/user-app/src/main.ts | 7 + apps/user-app/src/router/index.ts | 2 + apps/user-app/src/stores/appearanceStore.ts | 120 ++++ apps/user-app/src/stores/filtersStore.ts | 13 +- apps/user-app/src/utils/orderStatus.ts | 15 +- apps/user-app/src/views/CartView.vue | 10 +- apps/user-app/src/views/HomeView.vue | 27 +- apps/user-app/src/views/NewestView.vue | 2 +- .../src/views/expert/ChildOrdersView.vue | 2 +- .../src/views/expert/CustomerChoiceView.vue | 8 +- .../src/views/report/ReportDocument.vue | 4 +- apps/user-app/src/views/report/ReportView.vue | 4 +- .../src/views/search/CollectionView.vue | 2 +- apps/user-app/src/views/search/SearchView.vue | 64 +- .../src/views/user/AppearanceView.vue | 340 +++++++++ .../src/views/user/CompleteProfileView.vue | 2 +- .../user-app/src/views/user/DashboardView.vue | 29 +- .../user-app/src/views/user/FavoritesView.vue | 2 +- apps/user-app/src/views/user/LoginView.vue | 2 +- .../src/views/user/NotificationsView.vue | 2 +- apps/user-app/src/views/user/OrdersView.vue | 4 +- apps/user-app/src/views/user/ProfileView.vue | 4 +- apps/user-app/src/views/user/RegisterView.vue | 2 +- .../src/views/user/TransactionsView.vue | 4 +- apps/user-app/themeConfig/appearance.ts | 154 +++++ apps/user-app/themeConfig/colors.dark.json | 31 + apps/user-app/themeConfig/themeConfig.ts | 12 +- apps/user-app/vite.config.ts | 9 +- 102 files changed, 4602 insertions(+), 1053 deletions(-) create mode 100644 apps/user-app-v2/src/components/product/ProductMorph.vue create mode 100644 apps/user-app-v2/src/components/ui/ToggleSwitch.vue create mode 100644 apps/user-app-v2/src/composables/useProductMorph.ts create mode 100644 apps/user-app-v2/src/stores/appearanceStore.ts create mode 100644 apps/user-app-v2/src/views/user/AppearanceView.vue create mode 100644 apps/user-app-v2/themeConfig/appearance.ts create mode 100644 apps/user-app-v2/themeConfig/colors.dark.json create mode 100644 apps/user-app/src/components/product/ProductMorph.vue create mode 100644 apps/user-app/src/components/ui/ToggleSwitch.vue create mode 100644 apps/user-app/src/composables/useProductMorph.ts create mode 100644 apps/user-app/src/stores/appearanceStore.ts create mode 100644 apps/user-app/src/views/user/AppearanceView.vue create mode 100644 apps/user-app/themeConfig/appearance.ts create mode 100644 apps/user-app/themeConfig/colors.dark.json diff --git a/apps/user-app-v2/index.html b/apps/user-app-v2/index.html index eb8c2f2..853870b 100644 --- a/apps/user-app-v2/index.html +++ b/apps/user-app-v2/index.html @@ -5,13 +5,36 @@ - + + + + طلای منتزر -
+
diff --git a/apps/user-app-v2/src/App.vue b/apps/user-app-v2/src/App.vue index f0f54ae..952463c 100644 --- a/apps/user-app-v2/src/App.vue +++ b/apps/user-app-v2/src/App.vue @@ -29,6 +29,7 @@ @confirm="update()" /> + @@ -41,6 +42,7 @@ import MasterLayout from './layout/MasterLayout.vue' import FullPageLayout from './layout/FullPageLayout.vue' import IslandSheet from './components/ui/IslandSheet.vue' import ToastHost from './components/ui/ToastHost.vue' +import ProductMorph from './components/product/ProductMorph.vue' import { useRefresh } from './composables/useRefresh' import pkg from '../package.json' diff --git a/apps/user-app-v2/src/assets/styles/style.less b/apps/user-app-v2/src/assets/styles/style.less index 868bac3..ee301d0 100644 --- a/apps/user-app-v2/src/assets/styles/style.less +++ b/apps/user-app-v2/src/assets/styles/style.less @@ -367,8 +367,8 @@ input:focus-visible { } .ant-btn-primary[disabled], .ant-btn-primary[disabled]:hover, .ant-btn-primary[disabled]:focus, .ant-btn-primary[disabled]:active { - background: fade(@white, 20%); - color: fade(@white, 50%); + background: alpha(@white, 20%); + color: alpha(@white, 50%); border-color: transparent; } @@ -654,3 +654,108 @@ input:focus-visible { background: @hairline; } } + +/* ── Appearance: theme switch & reduced motion ───────────────── + Classes set on by stores/appearanceStore.ts (and, before the bundle + loads, by the boot script in index.html). */ + +// One-frame guard while the palette flips: without it every element with a +// colour transition animates from the old theme to the new one at once. +html.theme-switching *, +html.theme-switching *::before, +html.theme-switching *::after { + transition: none !important; +} + +// "Fast & light" mode. Not "no motion": the aim is speed without the app +// going dead, so +// - every transition still runs, capped at 120ms with no delay — taps still +// press, colours still change, sheets and pages still arrive, just quick; +// - keyframe animations run one near-instant iteration, so decorative loops +// (pulses, shimmers, illustration loops) stop at rest and one-shot reveals +// land in their final state; +// - backdrop blurs (recomputed every frame anything moves behind them) and +// permanent will-change layers are dropped; +// - .keep-motion opts working motion out: spinners, and the toast progress +// bar whose animationend is what closes the toast. vant's own +// spinner is excluded by class. +html.motion-reduced :where(*:not(.keep-motion, .keep-motion *, .van-loading, .van-loading *)), +html.motion-reduced :where(*:not(.keep-motion, .keep-motion *, .van-loading, .van-loading *))::before, +html.motion-reduced :where(*:not(.keep-motion, .keep-motion *, .van-loading, .van-loading *))::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + animation-delay: 0s !important; + transition-duration: 0.12s !important; + transition-delay: 0s !important; + scroll-behavior: auto !important; +} + +html.motion-reduced *, +html.motion-reduced *::before, +html.motion-reduced *::after { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + will-change: auto !important; +} + +// Glass that was only legible because of its blur gets a near-solid fill. +html.motion-reduced .tabbar__inner, +html.motion-reduced .toast { + background: alpha(@canvas, 97%) !important; +} + +/* ── Dark theme: third-party surfaces ────────────────────────── + ant-design-vue compiles its theme at build time with black text + (fade(@black, 85%)) and white fills; only its surface backgrounds were + switched to the theme tokens (themeConfig.ts). The text and input chrome it + leaves light-only are covered here. vant follows on its own through the + .van-theme-dark class. */ +html.theme-dark body { + color: @ink; +} + +html.theme-dark { + .ant-input, + .ant-input-affix-wrapper, + .ant-input-number, + textarea.ant-input { + color: @ink; + background-color: @canvas; + border-color: @hairline; + } + .ant-input::placeholder, + textarea.ant-input::placeholder { + color: @inkFaint; + } + .ant-input-affix-wrapper > input.ant-input { + background-color: transparent; + } + .ant-form-item-label > label, + .ant-drawer-title, + .ant-spin-text { + color: @ink; + } + .ant-form-item-explain-error { + color: @red; + } + .ant-empty-description { + color: @inkFaint; + } + .ant-drawer-header { + border-bottom-color: @hairline; + } + .ant-slider-handle { + background-color: @canvas; + } +} + +// Dark: the frosted glass (tab bar, toasts) is mostly clear by design, which +// on a dark page leaves nothing to separate it from what scrolls beneath. +// Give it a near-solid card fill and a hairline edge instead. +html.theme-dark .tabbar__inner, +html.theme-dark .toast { + background: alpha(@canvas, 94%) !important; + box-shadow: + inset 0 0 0 1px @hairline, + 0 18px 40px -14px rgba(0, 0, 0, 0.6) !important; +} diff --git a/apps/user-app-v2/src/components/Spinner.vue b/apps/user-app-v2/src/components/Spinner.vue index 5617189..76e0eb0 100644 --- a/apps/user-app-v2/src/components/Spinner.vue +++ b/apps/user-app-v2/src/components/Spinner.vue @@ -3,7 +3,8 @@ diff --git a/apps/user-app-v2/src/views/user/CompleteProfileView.vue b/apps/user-app-v2/src/views/user/CompleteProfileView.vue index 39b16d5..45a3e4a 100644 --- a/apps/user-app-v2/src/views/user/CompleteProfileView.vue +++ b/apps/user-app-v2/src/views/user/CompleteProfileView.vue @@ -153,7 +153,7 @@ function finish() { .cpf__error { margin: 12px 2px 0; font-size: 12.5px; - color: #dc2626; + color: @red; } .cpf__submit { diff --git a/apps/user-app-v2/src/views/user/DashboardView.vue b/apps/user-app-v2/src/views/user/DashboardView.vue index 934c0b3..058368a 100644 --- a/apps/user-app-v2/src/views/user/DashboardView.vue +++ b/apps/user-app-v2/src/views/user/DashboardView.vue @@ -116,7 +116,7 @@ سفارشات پیگیری سفارش‌های شما - + + + + طلای منتزر -
+
diff --git a/apps/user-app/src/App.vue b/apps/user-app/src/App.vue index adb20b1..ad0ee9e 100644 --- a/apps/user-app/src/App.vue +++ b/apps/user-app/src/App.vue @@ -21,10 +21,11 @@ @@ -34,10 +35,11 @@ + @@ -51,10 +53,13 @@ import MasterLayout from './layout/MasterLayout.vue' import FullPageLayout from './layout/FullPageLayout.vue' import DialogVantOneButton from './components/DialogVantOneButton.vue' import ToastHost from './components/ui/ToastHost.vue' +import ProductMorph from './components/product/ProductMorph.vue' import { useRefresh } from './composables/useRefresh' +import { useAppearanceStore } from './stores/appearanceStore' import pkg from '../package.json' const router = useRouter() +const appearance = useAppearanceStore() const { updateServiceWorker } = useRegisterSW() const getLayout = computed(() => { diff --git a/apps/user-app/src/assets/styles/style.less b/apps/user-app/src/assets/styles/style.less index 6874567..281a222 100644 --- a/apps/user-app/src/assets/styles/style.less +++ b/apps/user-app/src/assets/styles/style.less @@ -367,8 +367,8 @@ input:focus-visible { } .ant-btn-primary[disabled], .ant-btn-primary[disabled]:hover, .ant-btn-primary[disabled]:focus, .ant-btn-primary[disabled]:active { - background: fade(@white, 20%); - color: fade(@white, 50%); + background: alpha(@white, 20%); + color: alpha(@white, 50%); border-color: transparent; } @@ -632,7 +632,7 @@ img[lazy='error'] { position: absolute; inset: 0; transform: translateX(100%); - background: linear-gradient(90deg, transparent, fade(#fff, 65%), transparent); + background: linear-gradient(90deg, transparent, alpha(@canvas, 65%), transparent); animation: skeleton-sweep 1.3s ease-in-out infinite; } } @@ -688,3 +688,107 @@ img[lazy='error'] { transition: none; } } + +/* ── Appearance: theme switch & reduced motion ───────────────── + Classes set on by stores/appearanceStore.ts (and, before the bundle + loads, by the boot script in index.html). */ + +// One-frame guard while the palette flips: without it every element with a +// colour transition animates from the old theme to the new one at once. +html.theme-switching *, +html.theme-switching *::before, +html.theme-switching *::after { + transition: none !important; +} + +// "Fast & light" mode. Not "no motion": the aim is speed without the app +// going dead, so +// - every transition still runs, capped at 120ms with no delay — taps still +// press, colours still change, sheets and pages still arrive, just quick; +// - keyframe animations run one near-instant iteration, so decorative loops +// (pulses, shimmers, illustration loops) stop at rest and one-shot reveals +// land in their final state; +// - backdrop blurs (recomputed every frame anything moves behind them) and +// permanent will-change layers are dropped; +// - .keep-motion opts working motion out: spinners, and the toast progress +// bar whose animationend is what closes the toast. vant's own +// spinner is excluded by class. +html.motion-reduced :where(*:not(.keep-motion, .keep-motion *, .van-loading, .van-loading *)), +html.motion-reduced :where(*:not(.keep-motion, .keep-motion *, .van-loading, .van-loading *))::before, +html.motion-reduced :where(*:not(.keep-motion, .keep-motion *, .van-loading, .van-loading *))::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + animation-delay: 0s !important; + transition-duration: 0.12s !important; + transition-delay: 0s !important; + scroll-behavior: auto !important; +} + +html.motion-reduced *, +html.motion-reduced *::before, +html.motion-reduced *::after { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + will-change: auto !important; +} + +// Glass that was only legible because of its blur gets a near-solid fill. +html.motion-reduced .tabbar__inner, +html.motion-reduced .toast { + background: alpha(@canvas, 97%) !important; +} + +/* ── Dark theme: third-party surfaces ────────────────────────── + ant-design-vue compiles its theme at build time with black text + (fade(@black, 85%)) and white fills; only its surface backgrounds were + switched to the theme tokens (themeConfig.ts). The text and input chrome it + leaves light-only are covered here. vant follows on its own through the + .van-theme-dark class. */ +html.theme-dark body { + color: @ink; +} + +html.theme-dark { + .ant-input, + .ant-input-affix-wrapper, + .ant-input-number, + textarea.ant-input { + color: @ink; + background-color: @canvas; + border-color: @hairline; + } + .ant-input::placeholder, + textarea.ant-input::placeholder { + color: @inkFaint; + } + .ant-input-affix-wrapper > input.ant-input { + background-color: transparent; + } + .ant-form-item-label > label, + .ant-drawer-title, + .ant-spin-text { + color: @ink; + } + .ant-form-item-explain-error { + color: @red; + } + .ant-empty-description { + color: @inkFaint; + } + .ant-drawer-header { + border-bottom-color: @hairline; + } + .ant-slider-handle { + background-color: @canvas; + } +} + +// Dark: the toast's frosted glass is mostly clear by design, which on a dark +// page leaves nothing to separate it from what scrolls beneath. Give it a +// near-solid card fill and a hairline edge instead. +html.theme-dark .toast { + background: alpha(@canvas, 94%) !important; + box-shadow: + inset 0 0 0 1px @hairline, + 0 18px 40px -14px rgba(0, 0, 0, 0.6) !important; +} diff --git a/apps/user-app/src/components/DialogVant.vue b/apps/user-app/src/components/DialogVant.vue index 3bd4bce..9ffd7d1 100644 --- a/apps/user-app/src/components/DialogVant.vue +++ b/apps/user-app/src/components/DialogVant.vue @@ -17,7 +17,7 @@
{{ text1 }}
-
+
{{ text3 }}
@@ -44,12 +44,12 @@ withDefaults( display: none !important; } .van-dialog__confirm .van-button__text { - color: #23493c; + color: @primary; font-weight: 600; font-size: 15px; } .van-dialog__cancel .van-button__text { - color: #6b7280; + color: @inkMuted; font-weight: 600; font-size: 15px; } diff --git a/apps/user-app/src/components/DialogVantOneButton.vue b/apps/user-app/src/components/DialogVantOneButton.vue index efb6c65..70ad7c3 100644 --- a/apps/user-app/src/components/DialogVantOneButton.vue +++ b/apps/user-app/src/components/DialogVantOneButton.vue @@ -17,7 +17,7 @@
{{ text1 }}
-
+
{{ text3 }}
@@ -44,12 +44,12 @@ withDefaults( display: none !important; } .van-dialog__confirm .van-button__text { - color: #23493c; + color: @primary; font-weight: 600; font-size: 15px; } .van-dialog__cancel .van-button__text { - color: #6b7280; + color: @inkMuted; font-weight: 600; font-size: 15px; } diff --git a/apps/user-app/src/components/Spinner.vue b/apps/user-app/src/components/Spinner.vue index 5617189..76e0eb0 100644 --- a/apps/user-app/src/components/Spinner.vue +++ b/apps/user-app/src/components/Spinner.vue @@ -3,7 +3,8 @@ @@ -240,7 +240,7 @@ onMounted(async () => { diff --git a/apps/user-app/src/views/report/ReportView.vue b/apps/user-app/src/views/report/ReportView.vue index dad250c..84581a0 100644 --- a/apps/user-app/src/views/report/ReportView.vue +++ b/apps/user-app/src/views/report/ReportView.vue @@ -360,11 +360,11 @@ onBeforeUnmount(() => observer?.disconnect()) } .is-in { - color: #16a34a; + color: @green; } .is-out { - color: #dc2626; + color: @red; } .rep__more, diff --git a/apps/user-app/src/views/search/CollectionView.vue b/apps/user-app/src/views/search/CollectionView.vue index b86ff10..5227520 100644 --- a/apps/user-app/src/views/search/CollectionView.vue +++ b/apps/user-app/src/views/search/CollectionView.vue @@ -67,7 +67,7 @@ onMounted(async () => { diff --git a/apps/user-app/src/views/user/CompleteProfileView.vue b/apps/user-app/src/views/user/CompleteProfileView.vue index 72bbd08..600f824 100644 --- a/apps/user-app/src/views/user/CompleteProfileView.vue +++ b/apps/user-app/src/views/user/CompleteProfileView.vue @@ -153,7 +153,7 @@ function finish() { .cpf__error { margin: 2px; font-size: 12.5px; - color: #dc2626; + color: @red; } .cpf__submit { diff --git a/apps/user-app/src/views/user/DashboardView.vue b/apps/user-app/src/views/user/DashboardView.vue index 6305e97..a9620f0 100644 --- a/apps/user-app/src/views/user/DashboardView.vue +++ b/apps/user-app/src/views/user/DashboardView.vue @@ -130,8 +130,8 @@ @@ -146,7 +146,7 @@ @@ -208,11 +208,22 @@ const EXPERT_TILES = [ { id: 5, icon: 'vuesax-linear:profile-2user', title: 'مشتریان من', disabled: false }, ] -const list = computed(() => - session.isSalesExpert ? [...BASE_TILES, ...EXPERT_TILES] : BASE_TILES, -) +// Appearance (theme + reduced motion) — always last, and open to everyone: +// device preferences, not account data. See goToDetails. +const APPEARANCE_TILE = { id: 7, icon: 'vuesax-linear:brush', title: 'ظاهر', disabled: false } + +const list = computed(() => [ + ...BASE_TILES, + ...(session.isSalesExpert ? EXPERT_TILES : []), + APPEARANCE_TILE, +]) function goToDetails(id: number) { + // The only tile that needs no login. + if (id === 7) { + router.push({ name: 'appearanceSettings' }) + return + } if (!session.isLoggedIn) { data.loginNotify = true return @@ -260,7 +271,7 @@ onMounted(async () => {