Vue 3 / Bootstrap 5 / Vite migration with dark mode, performance & checkout fixes - #749
Open
KrzysztofPajak wants to merge 47 commits into
Open
Vue 3 / Bootstrap 5 / Vite migration with dark mode, performance & checkout fixes#749KrzysztofPajak wants to merge 47 commits into
KrzysztofPajak wants to merge 47 commits into
Conversation
…ue 3/Bootstrap 5 Replaces bootstrap-vue with an in-house Vue 3 compatibility layer (vueapp/src/compat) that reimplements the b-* components, $bvToast/$bvModal services, v-b-toggle/modal/tooltip directives and vee-validate 3's validation-observer/provider API used throughout the storefront views, so existing Razor templates keep working largely unchanged. Mechanically updates Bootstrap 4 classes/attributes to their Bootstrap 5 equivalents across views and theme CSS, and fixes several Vue 3 template incompatibilities uncovered along the way (bare <template> fragments, v-slot+v-if/v-else combinations, Vue.extend usage, stray v-if-else typo). Also fixes a handful of storefront bugs surfaced during testing: - checkout "Go to cart" / view-cart buttons not navigating (Vue 3 proxy intercepting bare `location = ...` assignments) - missing checkout button on the cart page (v-if-else typo hid the section) - add-to-cart popup layout/styling and quick view modal sizing/overflow - country -> state dropdown repopulation now driven by reactive data instead of raw DOM manipulation, which Vue 3 could wipe out on re-render Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t button spacing - ValidationProvider no longer listens to 'input' on <select> controls: native selects fire input before change, and Vue's v-model syncs on change only, so validating on input reactively re-rendered the select with a stale (empty) model value and silently discarded the user's country/state pick. - Selectors.cshtml: drop the stray Bootstrap 5 grid ".row" class from the category toolbar wrapper (BS5's ".row > *" forces width:100% on non-.col children, which stacked the sort/display/pagination controls vertically), and fix the mismatched "grid3x2-gap" icon name to the real bootstrap-icons "grid-3x2-gap". - b-icon compat: support the "scale"/"rotate"/"flip"/"shift" props BootstrapVue exposed, not just "font-scale". - b-card compat: render the "sub-title" prop (was silently dropped). - Register.cshtml: same BS5 ".row" grid regression fixed for the gender radios. - Checkout: add spacing between the Back/Confirm and "order details"/Continue button pairs, and fix a bare `location = ...` handler that no longer navigates under Vue 3. - Enable Razor runtime compilation in Development for faster iteration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dark mode (:dark-theme) mechanism: - Vue 3 never manages the root #app mount container's own attributes (only its innerHTML is compiled as the template), so the existing :dark-theme="darkMode" binding on #app was permanently inert. app.js now sets the dark-theme attribute imperatively on #app and body on toggle and on load from localStorage; added body[dark-theme] background. Missing text-color overrides (Bootstrap 5 sets explicit component colors via CSS variables instead of relying on inheritance, unlike BS4): - .table cells set color via --bs-table-color per BS5's ".table > :not(caption) > * > *" rule; added the missing --bs-table-color/ --bs-table-striped-color/--bs-table-hover-color/--bs-table-active-color dark overrides (fixes cart totals, product attributes table, and any other plain .table sitewide). - .list-group-item and .card set their own explicit color; added dark overrides for both. - .attributes .custom-control-description / .form-check-label (product attribute checkbox/radio option text) had no dark color at all. - .b-sidebar (cart/wishlist/compare side panels) had zero dark styling (background, close button, borders, .text-muted). Corrupted compound selectors from the BS4->BS5 rename script: - The blanket "custom-switch" -> "form-check form-switch" text substitution inserted a literal space inside compound class selectors (e.g. ".contrast-switch.custom-switch.b-custom-control-lg" -> ".contrast-switch.form-check form-switch.b-custom-control-lg"), turning one compound selector into an unrelated descendant selector that matched nothing. Restored the correct dot-joined selectors in common.css/.rtl.css. - BootstrapVue's switch used label ::before/::after pseudo-elements to draw the track/thumb (Vue 2); Bootstrap 5 draws the switch via a background-image on the input itself, so those pseudo-element rules and the icon's negative offset positioning no longer applied to anything real. Rewrote .contrast-switch to style the native BS5 switch input directly and lay the sun/moon icons out via flex order instead of absolute positioning tied to the old pseudo-elements. - .badge-primary/secondary/success/danger/warning/info/light/dark (removed in BS5, replaced by text-bg-*) are still emitted by admin-entered category "flag style" content; added compat aliases mapping them to the BS5 theme colors so existing data keeps rendering instead of going invisible (transparent bg + inherited text color). - .breadcrumb relies on BS5's default flex align-items: stretch; the enlarged home icon stretched its <li> taller than sibling items, misaligning the "Home" text baseline. Added align-items: center. - .block-account-navigation ul li svg targeted BootstrapVue's old inline-SVG icon output; the icon compat shim renders <i class="b-icon">, so the spacing rule never matched. Added the .b-icon selector alongside it. Also fixes cart sidebar item rows (name/price touching the thumbnail image) and the sidebar close button being unstyled (invisible on dark backgrounds). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move mobile header icons back to the top row instead of a fixed bottom bar (pre-existing behavior, not overflow-safe under BS5 defaults), and tighten spacing/icon sizes so nothing wraps below ~320px width. Fix HomePageBlog referencing New Products' column-width property (Np_Col) instead of its own (Pb_Col), causing blog posts to render two per row on mobile instead of stacking like the News widget. Hide the mobile "Filters" toggle button when a category has no specification/rating filters, since its only content would otherwise be the category-navigation block, which is intentionally hidden below the lg breakpoint - previously this left the sidebar opening empty. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vity bugs Bring the Theme.Modern plugin (now the default storefront theme) up to the same Vue 3/BootstrapVue-3/Bootstrap 5 baseline as Grand.Web's default theme: replace broken Vue.use(VueAwesomeSwiper) usage with a small swiper-vue3.js wrapper, fix template/prop-casing issues, and rework the product gallery mount so it survives #app's re-render instead of getting wiped. Fix category page dropdown/pagination/sorting/breadcrumb layout, sidebar close button visibility, quick-view attribute table layout (BS5's 3-level table selector needs a real <tbody>), wishlist sidebar item stacking, "GO TO CART" navigation from both the add-to-cart popup and the side cart, and grid/list view toggle spacing and behavior. Fix mobile blog posts rendering as bare "details" buttons, product image swipe/thumbnail-click for 2-image products, and category breadcrumb alignment on mobile. Fix the shopping cart REMOVE button: removing a row via its own delete button reliably crashed deep inside Vue's Fragment/keyed-list unmount internals, so the row is now hidden directly and the cart/totals state is refreshed from the server response instead of relying on the in-place reactive patch. Fix the on-hold toggle in the cart items table rendering outside the table: its label's ps-0 class removed the padding that Bootstrap's form-switch negative margin depends on, pulling the switch out past the column's left edge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The mobile header-nav (currency/tax/language selector strip) was still using the old fixed-bottom-bar pattern, independently of its .left-side-container parent's own positioning - it stayed pinned to the viewport bottom at all times, including once the user scrolled into the real page footer, permanently overlapping footer content. Drop the fixed positioning so it flows normally with the rest of the header instead. Hide the mobile "Filters" toggle (#mobile-filters-toggle) when .generalLeftSide has no visible content besides the always-present close button - previously it stayed visible even when a category has no specification/rating filters, whose only child (category navigation) is d-none below the lg breakpoint, so opening it revealed an empty panel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removing its fixed-bottom positioning (previous commit) put it back in normal flow as a sibling of the icon row inside the sticky mobile header, whose flex column is bottom-anchored - so the icon row got pushed down by the selector strip's height instead of staying at the very top like before. There's no other place for this strip on mobile, and it was already a barely reachable fixed corner widget, so just hide it below the lg breakpoint: restores the icons to the top and keeps the footer clear. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The footer's three <b-col> blocks only specified md/lg/xl breakpoint props, with no bare cols prop - Bootstrap's col-md-*/col-lg-*/col-xl-* classes only apply at those breakpoints and up, so below md (768px) these divs had no grid class at all, shrinking to their content width and packing to the left instead of stacking full-width. Their text was centered via text-align, but the narrow column box itself sat off to one side, making the whole About Us/Information/My Account blocks look uncentered on phones. Add cols="12" so they stack full width below md, same as the footer-bottom row already does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The drawer footer renders flycart.SubTotal behind v-if="flycartindicator > 0", but the count and the model are loaded independently - flycart only arrives when the drawer is first opened. That held together only as long as every writer set both at once; setting just the count made the footer render against a null model, and because a Vue render error tears down the whole app, changing a quantity on the cart page blanked the entire page. Require the model as well, matching what the wishlist drawer already did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every add-to-cart, compare, quick-view and bid button carried a hand-written
onclick="AxiosCart.someFunction('url', 'id', ...)" string, with routes and
localized text interpolated into JS source at each of 64 call sites. The
Vue-rendered variants were worse: they built that source by concatenation inside
a :onclick binding, so a single click went through two levels of escaping.
Buttons now declare what they do - data-cart-action plus data-url /
data-product-id / data-form / data-show-qty - and one delegated listener in
public.axios.js maps that onto the AxiosCart API. Delegation also covers markup
Vue renders later. The compare link text and the compare page URL moved to
window.grandRes / window.grandRoutes, emitted once by Partials/JsResources.
Fixed along the way:
- addbid built invalid JS; the template literal swallowed the form argument
- updateitem always read the quick-view form, which is a null lookup unless the
modal is open, and ignored the selector it was given
- ScriptTagHelper had no asp-append-version support, so a cached theme script
could keep running against markup that had already changed. The property is
bool? to match the built-in ScriptTagHelper, which binds the same attribute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each product box worked out its own add-to-cart, wishlist and quick-view URLs,
and did it twice: once as a 20-line Razor prelude in CatalogProductView, and
again as string concatenation inside the Vue templates that render the catalog
listings. The ShowQty and ForceRedirectionAfterAddingToCart branching was
duplicated across both themes.
ProductOverviewModel now carries AddToCartUrl, AddToWishlistUrl and QuickViewUrl,
filled in once by GetProductOverviewHandler after the price model is known. That
removes 40 URL expressions from the views, including all URL building from the
client side.
Link generation goes through the current HttpContext so the {language} segment
used by SEO-friendly URLs is carried over from ambient route values. Without it
every link falls back to the default language - the existing Url property had
that bug too and is fixed here. The same pattern is still present in the
cart/wishlist handlers, which build ProductUrl without a context.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each catalog listing wrapped its add-to-cart and wishlist buttons in a v-if="!Model.ShowQty" / v-else pair whose only purpose was to pick between two hardcoded URLs. Now that the model carries the URL, both arms render the same thing, so eight of the ten pairs collapse to a single button. The two pairs left in Theme.Modern stay: there the ShowQty arm really does render an extra text label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The read-only rating repeated the same 200-character <b-form-rating> tag in four views, twice with id='rating-inline2' inside a loop over reviews - so a customer with several reviews got duplicate ids. Partials/Rating.cshtml now owns that markup; the id is dropped because nothing referenced it. A partial rather than a ViewComponent: the rating needs no services, and a partial is both cheaper (measured on /sitemap with 5000 invocations: 1.6us per partial against 2.7us per ViewComponent) and equally overridable by a theme, which is what Theme.Modern already does for CatalogProductView itself. Ratings bound to Vue state are untouched - a partial renders once on the server and cannot follow a reactive value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vue-cli and eslint 8 are both end-of-life and accounted for all 28 npm audit findings (18 high); production dependencies were already clean. The toolchain is now vite + eslint 9 flat config: 164 packages instead of 581, and 6 findings, all from one brace-expansion advisory whose latest published 1.x is still flagged and which minimatch@3 under eslint pins us to. Two constraints the config has to honour, neither obvious: - the `vue` alias must resolve to vue.esm-bundler.js. Templates are the Razor markup itself, compiled out of the DOM at runtime; the default runtime-only build renders nothing. - output must be IIFE. The bundle is loaded by a plain <script src> and assigns window.Vue / window.bootstrap / window.axios. IIFE cannot code-split, so the vendor chunk is gone and both themes' Head.cshtml now load a single file - the entry is only 15 KB, so little caching value is lost. Also fixes a live bug found while measuring: `vue-cli-service build` emptied wwwroot/bundles first, deleting style.min.css and style.rtl.min.css - the production stylesheets Head.cshtml serves outside Development. Nothing generated them, so they were hand-maintained and had already drifted once (bc5b81b changed header.css and left them behind). They are now built from the same six sources, in Head.cshtml's cascade order, and emptyOutDir is off. The regenerated file differs from the committed one only in minifier style (:first-child vs :nth-child(1), selector order inside groups); no rule was lost. Verified: bundle boots with all globals set and no uncompiled templates, icon fonts resolve under /bundles/fonts/, both production stylesheets serve, matrix 38/38 across both themes and three viewports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ark mode Dark mode was applied from app.js on Vue's `mounted`, so every page painted light and then flipped, and it only ever worked in the default theme - Theme.Modern stayed light even with an explicit choice stored, because its #app carried no dark-theme binding and it had no switch at all. Partials/Rating aside, this is the first shared partial both themes render: Partials/ColorScheme runs inline in <head> and resolves the scheme before the first paint. localStorage now holds only an *explicit* choice; while it is absent the OS preference wins and is followed live, which is what a visitor running a dark desktop expects on a first visit. body and #app do not exist that early, so a short-lived MutationObserver tags them as soon as they are parsed - without it the ~330 hand-written [dark-theme=true] rules would still flash. `darkMode` is seeded in data() rather than mounted() in both themes' app.js: the watcher must not fire for the initial value, or it would persist a choice the visitor never made and silently stop following the system. Bootstrap 5.3's data-bs-theme is now driven too, so its own components theme themselves - that is what makes Modern dark without writing dark CSS for it. Verified with the matrix (56 checks): every "colour scheme is light, expected dark" failure is gone in both themes, for an explicit choice and for an emulated prefers-color-scheme: dark with nothing stored. Clicking the switch flips the scheme, persists it, and keeps Vue in sync. Still to do: Modern hardcodes light colours that now fight Bootstrap's dark surfaces (black text on #212529, #dee2e6 text on white); the matrix lists them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d values Modern hardcoded #fff backgrounds and #000/#1c1b1b text in 90 rules. Once data-bs-theme started theming Bootstrap's own surfaces, those fixed values fought it: black text landed on Bootstrap's dark #212529, and Bootstrap's light #dee2e6 text landed on backgrounds Modern forced to white. They now read var(--bs-body-bg) / var(--bs-body-color) (and var(--bs-secondary-bg) for the #eaf0fa panels), so they follow the scheme instead of needing a dark twin rule. Light rendering is unchanged - those variables resolve to the same values there, which the matrix's light passes confirm. Deliberately left alone: black text on a coloured button (.btn-info, .btn-light), which Bootstrap keeps light in both schemes, and white decorative shapes drawn over an overlay (page loader, back-to-top arrow, parallax banner). Matrix is 56/56 across both themes, three viewports and three colour schemes (light, explicit dark, dark from the OS preference). The contrast probe catches both directions, so a rule that had gone too dark in light mode would have failed the light passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported: quantity fields and dropdowns showed values that were barely visible. Two separate causes, both invisible to the verification matrix because its contrast probe only sampled elements with their own text node - <input> and <select> render a value instead, so form controls were never measured. Theme.Modern: .custom-select used the `background` shorthand for its arrow, which resets background-color to transparent. A <select> paints its option list with the UA default rather than any ancestor backdrop, so the themed light text sat on white in the popup. It now carries var(--bs-body-bg), a border that follows --bs-border-color instead of a near-black #1c1b1b, and a light arrow under data-bs-theme=dark since the SVG is a data URI and cannot inherit currentColor. Default theme: `[dark-theme=true] .form-select, input.form-control` forced a #efefef field. That paired a light field with the dark text the old hand-written rules supplied; once the text colour came from --bs-body-color it turned light-on-light at 1.13:1. Bootstrap 5.3 themes these controls itself, so the override is gone - in the RTL twin too, which is generated into style.rtl.min.css. The matrix now measures form controls, and flags any <select> whose own background is transparent - a structural check, because walking up to an ancestor background would have called the broken popup fine. 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Advancing a checkout step called scrollToSection, which scrolled to the top of #checkout-steps. The steps begin below the "Checkout" heading, so every step change pushed that heading off the top of the screen. It now targets .checkout-page, which wraps both the heading and the steps, and subtracts the header height when .header-nav is actually sticky or fixed - below 991px it is meant to be, and a static header overlaps nothing. The position is read from getBoundingClientRect + scrollY rather than offsetTop, which was measured against .cart-view (a positioned ancestor) and so was not a document coordinate at all. public.checkout.js also gained asp-append-version; without it the cached copy keeps running against changed markup, which has cost debugging time before. Verified at 1440 and 414: the heading ends 12px below the top instead of scrolling away. Matrix 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
public.axios.min.js and public.common.min.js were last touched in December 2023 and are stale minified copies of the scripts next to them. Nothing loads them: no view, no C# and no JavaScript mentions them, and no code builds a ".min" path at runtime the way the stylesheets have a Development/Production split. The generated style.min.css / style.rtl.min.css are a different thing and stay - Head.cshtml serves them outside Development, and the Vite build now produces them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Views have written asp-append-version="true" on <link> for a while and no stylesheet was ever stamped. The attribute was not ignored by accident: the built-in LinkTagHelper binds it too, but it only stamps a plain href, and these links carry asp-src, so there was nothing for it to act on. Our helper never had the property at all. It now mirrors ScriptTagHelper - IFileVersionProvider against the request PathBase, bool? because the built-in helper binds the same attribute and a plain bool breaks Razor compilation. The theme stylesheet links in both themes' Head now pass the attribute, so a CSS change reaches a returning visitor instead of losing to a cached copy. Verified: /theme/css/common/common.css?v=... serves 200. Known limit: plugin content is not stamped. IFileVersionProvider resolves against WebRootFileProvider, while /Plugins is served from a separate PhysicalFileProvider (UseGrandStaticFiles), so those paths are returned unchanged - Theme.Modern's own stylesheets still rely on cache headers. The attributes added there are harmless and become effective if plugin paths are ever made resolvable. Composing the plugin provider into WebRootFileProvider would fix it but would also expose the content root to the default static-file middleware, so it is deliberately not done. Matrix 56/56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The badges on the orders list painted a status name onto a pale tint - #d1ecf1, #fff3cd, #d4edda, #f8d7da - while Bootstrap's .badge sets --bs-badge-color: #fff. White on those tints is roughly 1.2:1, which is why the statuses were hard to read. Dark mode had a separate override forcing #212529 on top, so only dark looked right. They now use Bootstrap's subtle/emphasis variable pairs (info, warning, success, danger), which ship an accessible foreground and flip with data-bs-theme; the dark override is deleted as redundant. Measured in both themes and both schemes: every badge is now between 7.15:1 and 10.8:1. The class comes from the order status *name*, which is editable in admin, so a renamed or custom status matches none of the four rules and previously rendered white on nothing. The base .status.badge rule now carries its own secondary background and foreground, so an unrecognised status stays readable (13.0:1 light, 8.8:1 dark). Applied to both themes and to the RTL twin that feeds style.rtl.min.css. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reservations for day products are generated at the store local midnight and persisted as UTC, so their stored time of day carries the UTC offset, while the dates posted by the datepicker are always midnight. Booking compared the two for exact DateTime equality, so in any timezone other than UTC no period could ever be matched and every add-to-cart failed with "No free bookings in this period". Compare on the date part instead, which is what the calendar already shows the customer - the storefront derives the free days from the date part of the same UTC values. This also stops the period filters from dropping the last day of a range. The loop was copied in three places, so it now lives in one helper. Also guard the reservation datepicker callback: it wrote the response into #sku/#mpn/#gtin and .actual-price without checking they exist, throwing "Cannot set properties of null" on products that render none of them, and never updating the price when it has no discount markup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Vue 3 countdown shim rendered its slot as a bare fragment, but @chenfengyuan/vue-countdown always wrapped it in a span and the theme hangs the whole countdown on that wrapper: ".countdown-box > span" lays the parts out in a row and "> span span" puts the ":" between them. Without it the numbers were five separate spans, so the row collapsed and the separators disappeared entirely. Render the slot inside a span again. Bare numbers also read poorly, so each one now carries its unit. The separator no longer trails the "Time left" label, and the rtl sheet no longer emits a fourth separator for three gaps. The units are resources rather than literals: added to DefaultLanguage.xml for fresh installs and to en_240.xml with a migration for stores coming from 2.3. Stores already recorded as 2.4 are past that migration and need them imported by hand, since GetCurrentMigrations only runs versions strictly newer than the database's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolve conflicts: - Accept axios bump to ^1.18.0 from develop (^742) - Keep Bootstrap 5 / Vue 3 / Vite setup (discard old Bootstrap 4 / webpack entries) - Remove root package-lock.json (deleted in feature branch per Vite migration) - Auto-merged: IRepository method renames from #743 Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
Contributor
Merge conflicts resolved in commit
Build passes with 0 errors. |
…m/grandnode/grandnode2 into feature/vue3-bootstrap5-migration
Introduced Product and Site JSON-LD partials for SEO and Google rich results. Updated all schema.org microdata to use https. Product images above the fold now load eagerly for improved LCP. Removed redundant <template v-if="true"> wrappers and made minor accessibility and performance improvements.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Overview
Migrates the GrandNode 2 storefront from Vue 2 / BootstrapVue / Bootstrap 4 to Vue 3 / Bootstrap 5, replaces the vue-cli build pipeline with Vite, and ships a series of performance, accessibility and correctness fixes developed on top of the migration.
What changed
Vue 3 + Bootstrap 5 migration
Grand.Web) for the Vue 3 Composition API /<script setup>model and Bootstrap 5 markup conventions.b-sidebar,b-icon,b-modaletc. with plain Bootstrap 5 offcanvas / modal / icon equivalents.vue-cli/ webpack build with Vite; theme CSS is now generated by the Vite pipeline instead of being maintained by hand.Dark mode
<head>(before paint) and follows the OSprefers-color-schemesetting.data-bs-theme.--bs-body-color,--bs-body-bg,--bs-secondary-bg, semantic badge colours, etc.), so dark mode flips automatically..sr-onlycompatibility shim (Bootstrap 5 renamed it to.visually-hidden).Performance
Grand.Web;text/javascriptadded to the MIME list (static JS was served with that type and bypassed the default compressor).public.checkout.jsemitted from the Checkout view) is nowdefer-ed, unblocking the HTML parser.#appno longer carriesv-cloak; the full-viewport loading overlay now keys off.axios-request(XHR only) instead of blanking the page until the 400 kB bundle has run.fadeInsubset it actually uses, reducinglibs.cssfrom 392 kB to 319 kB.Cart & sidebar
onclicktodata-attributes consumed by a single delegated handler.#sidebar-cart,#sidebar-wishlist,#sidebar-compare).Checkout
CheckoutController.Startnow validates checkout attributes on every GET entry point (previously onlyIndexchecked; a cart with an unanswered required attribute reached the order-placement step before failing)..not-collapsed(BS4) to:not(.collapsed)(BS5).public.checkout.jsfixed a blank-page regression introduced by the parser-blocking fix.Accessibility
<main>,<header>and<nav>landmarks to the layout.common.skiptocontentresource string).wishlistQtycount now reads from adata-qtyattribute instead of localized display text (fixes amounted()crash on stores with wishlist disabled).Business logic — product reservations
ProductReservationExtensions.FindGroupToBookandInRentalPeriod..Date(date part only) so reservations stored at local-midnight survive round-tripping through UTC.Other fixes & improvements
LinkTagHelperversion-stamping applied consistently.ShowQtydead branches removed from catalog views.max-width/max-heightconstraints removed.NuGet & tooling updates
Microsoft.AspNetCore.OpenApi→ 10.0.10MessagePack→ 3.1.8Microsoft.FeatureManagement.AspNetCore→ 4.6.0Scalar.AspNetCore→ 2.16.16Azure.Monitor.OpenTelemetry.AspNetCore→ 1.6.0Azure.Storage.Blobs→ 12.29.1AWSSDK.Core/AWSSDK.S3→ 4.0.100.x / 4.0.101.xMicrosoft.OpenApi2.11.0 added (required by updated OpenApi package)global.jsonpinned to .NET 10 SDK; README prerequisites table updated.Migration DB
MigrationUpdateResourceString(version 2.4) that importsen_240.xmlresource strings (includescommon.skiptocontent).Testing