Skip to content

Vue 3 / Bootstrap 5 / Vite migration with dark mode, performance & checkout fixes - #749

Open
KrzysztofPajak wants to merge 47 commits into
developfrom
feature/vue3-bootstrap5-migration
Open

Vue 3 / Bootstrap 5 / Vite migration with dark mode, performance & checkout fixes#749
KrzysztofPajak wants to merge 47 commits into
developfrom
feature/vue3-bootstrap5-migration

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Aug 2, 2026

Copy link
Copy Markdown
Member

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

  • Rewrote all storefront components (Grand.Web) for the Vue 3 Composition API / <script setup> model and Bootstrap 5 markup conventions.
  • Removed BootstrapVue (a Vue 2-only library); replaced b-sidebar, b-icon, b-modal etc. with plain Bootstrap 5 offcanvas / modal / icon equivalents.
  • Replaced the vue-cli / webpack build with Vite; theme CSS is now generated by the Vite pipeline instead of being maintained by hand.
  • Committed the Vite build output so the storefront runs without Node being installed.

Dark mode

  • Colour scheme is resolved in <head> (before paint) and follows the OS prefers-color-scheme setting.
  • Theme.Modern gains a full dark-mode palette keyed on data-bs-theme.
  • All hardcoded hex colours in Theme.Modern CSS replaced with Bootstrap CSS custom properties (--bs-body-color, --bs-body-bg, --bs-secondary-bg, semantic badge colours, etc.), so dark mode flips automatically.
  • Added a .sr-only compatibility shim (Bootstrap 5 renamed it to .visually-hidden).

Performance

  • Response compression: registered Brotli and Gzip providers explicitly and enabled over HTTPS for Grand.Web; text/javascript added to the MIME list (static JS was served with that type and bypassed the default compressor).
  • Deferred script loading: every script that depends on the bundle (including public.checkout.js emitted from the Checkout view) is now defer-ed, unblocking the HTML parser.
  • FCP / v-cloak: the root #app no longer carries v-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.
  • animate.css removed from the shared bundle (95 kB); Theme.Modern ships only the fadeIn subset it actually uses, reducing libs.css from 392 kB to 319 kB.
  • Server-side catalog listing: the first page of catalog results is rendered on the server so it is visible before JavaScript runs.

Cart & sidebar

  • Fixed Vue 3 reactivity bugs in the cart drawer (item additions/removals were not reflected).
  • Cart actions moved from inline onclick to data- attributes consumed by a single delegated handler.
  • Cart drawer body guard moved to the drawer model (was previously only on item count).
  • Sidebar IDs unified (#sidebar-cart, #sidebar-wishlist, #sidebar-compare).

Checkout

  • CheckoutController.Start now validates checkout attributes on every GET entry point (previously only Index checked; a cart with an unanswered required attribute reached the order-placement step before failing).
  • OPC accordion selector updated from .not-collapsed (BS4) to :not(.collapsed) (BS5).
  • Checkout heading stays on screen when a step opens.
  • Deferred public.checkout.js fixed a blank-page regression introduced by the parser-blocking fix.

Accessibility

  • Added <main>, <header> and <nav> landmarks to the layout.
  • Added a skip-to-content link (common.skiptocontent resource string).
  • wishlistQty count now reads from a data-qty attribute instead of localized display text (fixes a mounted() crash on stores with wishlist disabled).

Business logic — product reservations

  • Extracted duplicated reservation-group-finding logic into ProductReservationExtensions.FindGroupToBook and InRentalPeriod.
  • Fixed day-reservation booking outside UTC: comparisons now use .Date (date part only) so reservations stored at local-midnight survive round-tripping through UTC.

Other fixes & improvements

  • Server-side star ratings rendered through a single overridable partial.
  • Auction countdown layout and units restored.
  • LinkTagHelper version-stamping applied consistently.
  • Unreferenced minified theme scripts deleted.
  • ShowQty dead branches removed from catalog views.
  • Catalog action URLs built in the handler, not in the views.
  • Mobile footer overlap and stray empty Filters button fixed.
  • Mobile header layout and blog stacking fixed.
  • Navbar logo max-width/max-height constraints removed.
  • Footer columns stack full-width on mobile.
  • Mobile currency/language selector hidden correctly on scroll.

NuGet & tooling updates

  • Microsoft.AspNetCore.OpenApi → 10.0.10
  • MessagePack → 3.1.8
  • Microsoft.FeatureManagement.AspNetCore → 4.6.0
  • Scalar.AspNetCore → 2.16.16
  • Azure.Monitor.OpenTelemetry.AspNetCore → 1.6.0
  • Azure.Storage.Blobs → 12.29.1
  • AWSSDK.Core / AWSSDK.S3 → 4.0.100.x / 4.0.101.x
  • Microsoft.OpenApi 2.11.0 added (required by updated OpenApi package)
  • global.json pinned to .NET 10 SDK; README prerequisites table updated.

Migration DB

  • Added MigrationUpdateResourceString (version 2.4) that imports en_240.xml resource strings (includes common.skiptocontent).

Testing

  • All existing unit tests pass.
  • Storefront renders without JavaScript (server-side HTML visible before hydration).
  • Checkout flow: all six steps render; shipping address, shipping method and payment method each submit and advance correctly.
  • Dark-mode toggle verified in Theme.Modern.

KrzysztofPajak and others added 30 commits July 29, 2026 08:25
…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>
Copilot AI review requested due to automatic review settings August 2, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merge conflicts resolved in commit 345549b. The 5 new commits on develop were merged in:

Build passes with 0 errors.

Copilot AI review requested due to automatic review settings August 2, 2026 17:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings August 2, 2026 18:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

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.
Copilot AI review requested due to automatic review settings August 2, 2026 18:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings August 2, 2026 18:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings August 2, 2026 18:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@sonarqubecloud

sonarqubecloud Bot commented Aug 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
2.0% Coverage on New Code (required ≥ 80%)
15.5% Duplication on New Code (required ≤ 3%)
D Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copilot AI review requested due to automatic review settings August 2, 2026 19:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings August 3, 2026 14:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants