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 () => {