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.
Remove DialogVant/NotifyVant and add Toast and Sheet components with
useToast, useReveal, and usePageTransition composables. Rework
Navigation, RegisterView, and styles across both apps; rebuild dist assets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports "تکمیل پروفایل" end to end (D22).
API
- POST customer/me/complete-profile — three required document ids, the
customer taken from the token, verification step set to the reference's
LIVE value (approved outright; its `= 1` line is commented out at :83 in
favour of `= 2` at :85). Both SMS messages byte-identical and non-blocking
per D20.
- POST customer/me/documents — the reference let the app upload through the
same file/create the admin used, inside its auth:api-customer group
(routes/api.php:95); the target's copy is admin-guarded, so the page could
not have uploaded at all. Reuses AdminUploadsService unchanged, so both
paths share one size limit, MIME whitelist and naming scheme (UPL-01).
- PROFILE_ADMIN_MOBILES replaces the reference's four hardcoded personal
numbers; blank means nobody is notified and the upload still succeeds.
Three reference behaviours deliberately NOT ported, all in ISSUES.md:
- SEC-14: `$customer->update($request->all())` mass-assigned the whole request
body, letting the app write its own credit limit, making charge, gold debt,
active flag or mobile. Replaced with a three-field whitelist.
- SEC-15: four real personal mobiles hardcoded in the source.
- APP-03: refresh()'s unterminated setInterval page reload.
Apps (both, template+script byte-identical, per-app styling only)
- CompleteProfileView + SingleImageUploader
- route /complete_profile, redirecting home once the documents are in, as the
reference's guard did (router/index.ts:266)
- the entry point on the profile screen, shown only while they are missing
(profile.vue:96-102)
Also (D23): JWT_CUSTOMER_SECRET now warns loudly at boot when unset. It falls
back to JWT_SECRET, which signs app and admin tokens with the same key and
leaves the `typ` check as the only barrier — and an expert's app token carries
their AdminUser id in `sub`. Warn rather than refuse to boot (D19, lowest
risk); DELIVERY.md lists it as the first pre-release step (REL-03).
Verified: build 5/5 · jest 45/45 · complete-profile 38/38 (new) ·
expert login 36/36 · apps smoke 43/43 · admin smoke 43/43 ·
B2B scan 34 ok/0 broken · typecheck clean on all four apps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both expert screens are built in user-app and user-app-v2, with byte-identical
template and script and a per-app style block only — verified by diffing
everything above <style>:
ChildOrdersView "سفارش مشتریان" — legacy ChildOrders.vue. Keeps the
reference's title, search, infinite paging, and per-card item count, total
weight, customer name and the fixed «تحویل مسئول فروش» chip (the endpoint
only returns that status, ApiMobileController:766).
CustomerChoiceView "لیست مشتریان" — legacy customerChoice.vue. Keeps its
title, per-search re-query and the reference's choice to LIST inactive
customers (its active() scope is commented out at :187).
Reachable from inside the app, not just by URL: routes carry the reference's own
paths and names, the dashboard grows the two entries only for an expert (v1 as
tiles, v2 as its own rows), and a router guard sends an ordinary customer back
to their dashboard. None of that is relied on for safety — the server scopes
both endpoints on the AdminUser id in the token and answers 403 otherwise.
Not ported, both recorded in D21: the act-as api_token minting (:197-201) and
the child-order edit drawer, which recomputed an order's money from client-sent
values. The card opens a read-only summary instead.
APP-02 fixed: user-app-v2 gained getHomeSlider and now prefers admin-configured
slides, keeping its category-derived hero as the fallback and its own design.
Session stores stay byte-identical across the apps.
build 5/5, jest 45/45, expert login 36/36, apps smoke 43/43, typecheck clean.
[ref: henza_pwa/src/views/user/src/components/ChildOrders.vue, customerChoice.vue]
[ref: henza_pwa/src/router/index.ts:171-188]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D20: OTP_MASTER_CODE login bypass read only from the environment, disabled when
unset, audited on every use, with a boot banner while armed. SMS no longer
aborts the boot in production — it warns and declines silently, so no message
can stop an operation.
D19: port Utility::faTOen onto admin-entered mobiles (before the uniqueness
lookup, which could previously be bypassed with Persian digits), and the
create-only welcome SMS, byte-identical to the reference and fire-and-forget.
user-app-v2 was missing the cart stock ceiling entirely: capQty/maxQty, the
un-orderable guard and the qty clamp are ported, leaving v2's own design intact.
The two cart stores are now byte-identical. isVariantOrderable and the
orderable-first default selection ported too.
jest 45/45; typecheck clean on api, admin-panel, user-app, user-app-v2.
[ref: henza_server/app/Http/Controllers/Adm/AdminController.php:333,348,364,378-387]
[ref: henza_server/app/Models/Utility.php:26]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Extract product image gallery into a dedicated GallerySlider component
with swipe/tap support
- Add a teleported fullscreen image preview (tap to open, thumbnail strip,
body scroll lock) to SingleProduct
- Restore the Trust section that was accidentally commented out
- Swap back/favorite button positions and recenter the image counter
- Fix Icon.vue: tempIcon was a static ref set once at setup, so it never
updated on prop change when Vue reused the component instance (e.g. cart
qty steppers toggling trash/minus icons); switched to computed + watch
- Rename brand text from "منتزر" to "هنزاگلد"
- Fix a CSS typo in user-app-v2 SingleProduct (rgba alpha value)
- Rebuild dist assets
- SingleProduct.vue: rebuild gallery/header/action bar as a self-scrolling
layout with a sticky photo, solid-on-scroll header matching PageBar, and
a full-screen image preview (blurred overlay, centered thumbnail strip)
- Add GallerySlider.vue: pointer-based swipe carousel isolated in an
LTR subtree, fixing RTL/transform sign mismatch that caused slide jumps
- Fix Icon.vue: icon was read into a plain ref once at setup and never
updated on prop change; switch to computed so icon swaps (e.g. cart
qty stepper trash/minus toggle) actually re-render
- Fix quantity stepper DOM order in SingleProduct.vue/CartItem.vue so
plus/minus render correctly under RTL flex
- prisma/assign-downloaded-images.js: seed real product photos (FEATURED/
FEATURED_MOBILE/GALLERY/GALLERY_MOBILE) onto the 5 placeholder products
DashboardView ("میز کار"): Rebuilt from scratch with a new structure, distinct from the old card+grid layout but visually aligned with HomeView (same blurred bar, @ink/@wine palette, staged reveal animation on mount):
Single hero card (ink→wine gradient) showing balance, gold/fiat debt, and identity — no quick-action icons inside it anymore.
Live gold rate folded into a slim inline row at the bottom instead of a titled section.
Iterated shadow/spacing on the hero card per feedback (softer, less clipped).
DialogVant / DialogVantOneButton (shared modal components, used in 7+ call sites: Dashboard, Cart, SingleProduct, Register, App): Replaced Vant's default dialog chrome with the app's own rounded-card language — circular icon badge for the illustration, pill-shaped primary/secondary buttons. No call-site markup changed.
LoginView: Rebuilt from scratch — full-bleed dark hero (brand + step copy) overlapped by a floating white form sheet, animated crossfade transition between the mobile-number and OTP steps, same staged entrance animation as Home/Dashboard. All auth logic (API check, OTP stub, session login) unchanged.