Vue 3 / Bootstrap 5 / Vite migration with dark mode, performance & checkout fixes - #749
Merged
Merged
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>
core.js conflated two things under one misleading name. The island
machinery is how the storefront runs today; only the rest is owed to
Vue 2 era templates. Separating them makes the remaining debt countable.
runtime/islands.js gets what is architecture: mountIslands,
createStorefrontApp, registerComponent, getRootVm/getIslands, the
onAppCreate/onBeforeRootMount/onRootReady hooks and bringUpIslands.
compat/ keeps only what Razor templates force, each stating what would
retire it: globals.js (the window fall-through Proxy that 68 .cshtml
files depend on, plus foldProps), view-model.js (Options-API shaped
reactive state, and the cross-island $refs proxy), vue-global.js (the
window.Vue surface the theme script files call). Dependencies run one
way, no cycles.
new LegacyVue({...}) becomes createViewModel({...}) at all 27 call
sites, and LegacyVue.component becomes registerComponent. Drops $mount,
$watch and $bvModal, none of which had a caller - window.$bvModal was
never assigned, so that getter always returned undefined. The
new Vue({ el }) guard stays: it is a diagnostic for plugin authors, not
dead code.
createViewModel now applies foldProps, which makeStateVm did not, so
root-level props are no longer silently dropped - Theme.Modern's
home.js declares its whole home-page model that way and was getting
undefined.
Also retires a BootstrapVue leftover: both SearchBox views sent
$root.$emit('bv::hide::tooltip') into a $root that has no $emit, so
every touch on the voice-search button threw. They now call
hideTooltip($event.currentTarget), published next to the delegated
Tooltip in main.js.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `window` fall-through Proxy in compat/globals.js exists for one reason: 68 .cshtml files address their view-model by bare global name, and Vue 3 has no `with(this)`. Retiring it by moving those templates into components would mean giving up server-rendered markup and the @loc strings baked into it - the opposite of the direction the catalog listings took. An island can now declare what it needs instead: `vue-island="contactus"` mounts with the shell plus that view-model layered on top, so the template resolves the name as ordinary component data and needs no fall-through. The markup stays in Razor, unchanged apart from the attribute. The layering is a Proxy over the shell rather than a copy. `{ ...shell }` would hand each island a private snapshot and the drawers would stop agreeing on the cart; reads fall through to the shell and writes still land on it, so sharing is exactly as before. Verified with the global deleted: the binding kept working from island data, and a darkMode write still reached window.vm. Degrades safely in both directions. An island that declares nothing behaves as it always did, and a name that is not in the registry logs a warning and falls through to the window Proxy - i.e. to today's behaviour - because the data proxy's `has` trap reports the key as absent and Vue moves on to globalProperties. Registration goes through views/state.js, which builds 26 of the 47 [data-grand-vm] islands, so most of the remaining view-models become declarable without further changes. Contact and apply-vendor declare theirs here; the rest follow one at a time. Verified on /contactus: island mounted, binding proven independent of the global, form submits, and home, category and product pages - none of which declare anything - still mount every island with a clean console. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five more names off the window fall-through Proxy, using the mechanism from 87a7302. Which island roots to mark was not guessed: the raw server HTML was scanned for attributes mentioning each global and walked up to the nearest [vue-island], because Vue strips the directives from the DOM once it compiles an in-DOM template, so the live page cannot answer this. Each is proven the same way - delete the global on a live page, write through the island's data and watch the input follow: searchbox #searchBoxContainer (on every page) askquestion #contact-us-product productreviewsoverview .product-review-box productreviews .product-details-page and #ModalProductReview vmwishlist #wishlistPage (both themes) productreviews is the interesting one: it is declared on two islands, which share the one registered object rather than getting a copy each. Submitting a review from inside the modal moved the count on the page behind it, 1 -> 2. Verified beyond the bindings: search autocomplete still returns results and renders its dropdown; review submission succeeds end to end; home, category, cart, compare and wishlist mount every island with a clean console. Theme.Modern shares the wishlist page, so it is marked too. Note the shape of the search-box change - the marker sits on the container, not on the <validation-observer> below it. A component tag cannot be a mount point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The scan in 8f79cb8 walked the parsed document with querySelectorAll, which never enters a <template>: its children live in a DocumentFragment, not in the tree. Every reference written inside `<template v-for=...>` was therefore invisible, and that is most of the compare page. Rescanned recursively, descending into template.content. Two findings: - compare products reference `specificationAttributes` from inside nested <template v-for> blocks, on `.compare-products-page` in both themes; - `.product-review-box` reads `productreviews` as well as the overview model it already declared, and had been resolving that one through the window Proxy. Both are declared now. Verified on the compare page with the global deleted: trimming the model through island data collapsed the specification rows 5 -> 1, so the table renders from the island, not from window. Not touched, deliberately: `voicenavigator` and `vendorreviewsoverview` render nothing in this store - voice navigation is off in the settings and the vendor review block did not appear - and neither partial carries its own island marker, so their enclosing island would have been a guess. `merchandisereturns` sits on a page behind the login. Left on the Proxy until they can be checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Left on the window Proxy in cc19042 because the page sits behind a login and behind a delivered shipment - IsMerchandiseReturnAllowedQueryHandler only lets an order through once a shipment carries a DeliveryDateUtc, so none of the seed orders offered the button. With a returnable order to hand, the page renders and `.return-request-page` is the only island holding the seven references. Proven the same way as the rest: with the global deleted, writing through the island's data moved the quantity select "" -> "1" -> "0". The first attempt read as a failure and was not - the model took the value, but the item's dropdown only offers 0 and 1, so setting "2" correctly left nothing selected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_Layout renders <vc:voice-navigation> straight into <main>, which deliberately carries no island so that a broken block takes down itself rather than the whole page body. The component's root is a bare <template v-if="voicenavigator.allowed">, and markup no island compiles renders as *nothing*: no error, no element, nothing in the DOM to show for it. The voice button has been dead since <main> was split. It stayed invisible because the feature is off in the seed store. With VoiceNavigation enabled the block still did not appear, which is what exposed it. Wrapped in a plain div carrying the island, because the <template v-if> cannot be the mount point itself - Vue reads a mount point's innerHTML, so a directive on it is ignored. The [data-grand-vm] script stays outside the wrapper: initViews() has consumed it before this mounts, and Vue would otherwise re-render it. The view-model is declared on the wrapper rather than left to the window Proxy, so this lands as part of the same migration. Verified with the global deleted: toggling `recording` through island data opened the alert, put the focus class on the button and closed it again. #voicePageButton is now in the DOM on every page, where before there was no element at all; home, category, product, cart and contact all still mount every island with a clean console. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same failure as 13ceb5c, found the same way. Nothing on the vendor page header carries an island, so this partial was markup no island compiled: its <template v-if> rendered nothing and <b-form-rating> stayed an unknown element - no error, no stars, no review count. It hid behind a per-vendor field rather than a store setting. The block only renders when a vendor has AllowCustomerReviews set, and both seed vendors had it off, so nothing on the page hinted the markup was dead. The partial's own root takes the marker: its itemprop attributes are Razor's and it carries no Vue directive, so it is a valid mount point. Both themes use this one file - Theme.Modern's Vendor.cshtml resolves the same partial. Verified with the global deleted: writing the model through island data moved the block from "0.00 Be the first to review this vendor" to "5.00 4 review(s)" and recomputed `rating` 0 -> 5. The component resolves now - five stars in the DOM and no leftover <b-form-rating> tag. Vendor, category and home mount every island with a clean console. That closes the small view-models: only the catalog and vmorder clusters, and the shell itself, still reach through the window Proxy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The biggest consumer of the window Proxy, 35 files, and it needed none of the architectural work I had started and reverted earlier. Taking `catalog` off the fall-through is a declaration like any other; rebuilding how the listing renders is a separate question that this does not touch. The earlier scan reported `catalog` in "NO ISLAND" on every page, which sent me looking for a problem that was not there - plain substring matching also hits URLs and class names containing "catalog.". Matching /\bcatalog\.[A-Za-z]/ instead gives a clean answer, the same on all five listing pages: the listing island and the left column, plus the vendor page's tabs block. Declared on `#catalog-products` in all six layouts, on the vendor page's `.col-12` tabs island (it renders catalog.Model.Description), and conditionally on `_TwoColumns`' left column. Conditionally because that column is shared with every other two-column page and only the catalog layouts define the `filters` section that reads the view-model; declaring it unconditionally would ask for a view-model that is not there and log a warning per page. Verified on /blog: the attribute renders empty, no warning, all islands up. Verified with the global deleted: the listing and the sidebar resolve the *same object*, not copies - which is what keeps filters and products in step - and the client takeover ran entirely through island data, flipping clientRendered and rendering the products. Through the UI: sorting A-Z then Z-A reversed the five notebooks exactly, and a specification filter cut the list 5 -> 2 with the "currently filtered by" panel appearing. Brand, vendor and search pages all mount every island with a clean console. Theme.Modern is not covered here - its #catalog-products divs carry no island marker at all, so its roots are elsewhere and it is not the active theme. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Modern's roots are not Grand.Web's: its #catalog-products divs carry no island marker at all, and the markers sit on the page wrapper instead - .catalog-island for brand, collection and tag, .page.category-page, .page.search-page, .page.vendor-page - plus the left column in its own _TwoColumns, conditional on the `filters` section for the same reason as the default theme. Modern also builds its view-model in a separate module, views/catalog-modern.js, which the first pass missed. The warning that produced is worth keeping in mind as evidence the fallback works: both islands reported an unknown view-model and then rendered correctly anyway, through the window Proxy, with all twelve islands up and ten products on screen. Verified with the global deleted: the page island and the sidebar resolve the same object, and sorting Z-A to A-Z through the select reordered the model and the DOM matched it exactly. Brand, vendor and blog pages mount every island with a clean console; blog renders the sidebar's declaration empty, as intended. Modern has no `clientRendered` - it draws every product through Vue and has no server-rendered first page to hand over from. That difference is unchanged here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picking a date for a reservation product in the quick-view modal threw "Failed to construct 'FormData': parameter 1 is not of type 'HTMLFormElement'" from Reservation.onDatePickerSelect, and the exception landed before the axios call - so the price, SKU and availability refresh that the handler exists to do never ran at all. The cause is the pair of ids. A product page renders `product-details-form-<productId>`; quick view renders the same form under the bare `product-details-form`. onDatePickerSelect only ever looked for the suffixed one, found nothing in the modal, and handed null to FormData. AxiosCart already knew this rule and applied it in two places, so the fix is to stop keeping three copies of it: theme/product-form.js now owns activeProductForm(), and addproducttocart_details, updateitem and onDatePickerSelect all call it. The reservation path also gets the null guard the cart paths already had, so a missing form skips the refresh instead of throwing. Same family as the bug in the memory notes: the modal is always in the DOM now - BootstrapVue's b-modal only rendered its content while open - so code written then can be wrong about which form is on screen. Verified in the quick view: both dates selected, no console errors, and productdetails_attributechange now returns 200 where nothing was sent before. On the full product page: date set and the same request fires. Add to cart still works from both the modal and the product page, which is what shares the helper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Takes the last big name off the window fall-through Proxy in compat/globals.js. All 18 views that address `vmorder` are partials rendered inside two roots, and both already carried a bare `vue-island`, so naming those roots covers the lot. The checkout `vmorder` is built by public.checkout.js, which is not part of the bundle, so registerViewModel is exposed on the window.Vue facade for it. That script is asp-order 300 and the theme app.js that mounts the islands is 900, so the registration always lands first. Verified by deleting window.vmorder and mutating a rendered property on both pages: the DOM still updates, so the templates resolve the name from the declaration rather than the Proxy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
displayPopup did `this.$refs[el].show()`, a BootstrapVue `<b-modal ref>` reaching for a component instance. The partials it renders carry no `ref` at all and a Bootstrap 5 modal is a plain element with no `.show()`, so the lookup was undefined, the `if (modal && modal.show)` guard swallowed it, and the privacy-preference and newsletter-category popups mounted into the DOM without ever opening. Query the modal out of the container rather than by id: displayPopup gives the container the same id as the modal inside it, so getElementById hands back the wrapper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These carried Vue directives but no `vue-island`, and `<main>` carries
none either, so nothing ever compiled them. Theme.Modern already had the
marker on its equivalents - only the default theme was missing it.
The damage was invisible, which is how it survived: an uncompiled
`<template v-if>` renders nothing at all. The blog post page advertised
"Comments (1)" and showed none - the view-model held the comment, the
list simply never rendered. News items had the same comment list.
Password recovery and the wishlist e-mail form lost their whole
client-side validation, with `{{ errors[0] }}` sitting raw in the DOM.
Same class of bug as the voice navigation and vendor rating fixes: a
component tag outside an island stays an unknown element and a
`<template>` disappears, neither of which raises anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every view-model name a template binds is now declared on the island that renders it, so it resolves as ordinary component data instead of through the window fall-through Proxy in compat/globals.js. Most of it was naming islands that already existed: the account forms, the modals for order notes, e-mail-a-friend and wishlist e-mail, and Theme.Modern's twelve home-page sections, which had been sharing the one #home-page island and now each carry their own. Three view modules published to `window` without registering - comments.js (blog, news), country-state-form.js (customer) and contact-form.js (contactvendor, emailafriend). They register now; the console warning is what found them. The vendor tabs build their declaration conditionally, like _TwoColumns does: the reviews and contact tabs are optional and declaring a view-model that was never registered warns on every other vendor page. `productGallery` is deliberately not declared - Theme.Modern's app.js reads it off `window` and no template binds it. This does not retire the Proxy yet: 23 template expressions still reach it for `vm`, `window` and `bootstrap`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With AllowUsersToChangeEmail off, Account/Info renders the email input readonly but still validated it with `required|email`. An address that fails the format rule then locks the page: the rule rejects the value, and the visitor has no way to change what it rejects, so the account can never be saved again. That state is reachable today because the same field is validated differently depending on where you meet it - Register.cshtml uses `required` alone, so an address the account page will later reject can be registered without complaint. This is what a customer hit with an address ending .pl1. The `email` rule is now applied only while the field is editable. Three views had the same shape and all three are fixed: Account/Info, Account/Partials/EditSubAccount and Shared/Partials/CreateOrUpdateAddress (which keys off DisallowUsersToChangeEmail). `required` stays - the value is server-supplied, so it never fires, and it still means something on the editable path. Not changed here: Register still validates with `required` alone. Adding `email` there would make the client stricter than the server, which accepts these addresses, so aligning the two is a separate decision. Verified with the setting switched on, so the field is editable and the rule applies: the markup renders rules="required|email", .pl1 shows "Wrong email", and correcting the address clears it - which is the way out that did not exist before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pattern ended `\.[A-Z]{2,4}$`, so every address on a longer TLD failed -
.online, .travel, .museum, .company, .digital. Not a corner case: the rule backs
the contact form, ask-a-question, email-a-friend, addresses, the account page and
sub-accounts, so those customers simply could not submit any of them.
Found while tracking down a different report on the same field and confirmed in
the browser: someone@example.online was rejected with "Wrong email" before the
change and accepted after.
The cap is gone; the TLD is still letters-only, so a domain with a digit in it -
the .pl1 address from that report - is rejected exactly as before. Verified on
the account page: .pl1 still errors, .online and .museum pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by scanning every listing, account and catalog page for identifiers that a template uses as an expression root but its island does not declare - the ones still reaching through the window Proxy. Only two real gaps were left, both on the vendor page: the rating summary, which declared the overview model but also reads vendorreviews, and #ModalVendorReview, which declared nothing at all and uses it ten times. The view-model itself needed no work: it is built by the shared `reviews` module under the name "vendorreviews", which already registers what it creates. Verified with the global deleted: the modal resolves vendorreviews from island data, the overview still renders its five stars - outlines, because the vendor has no reviews yet - and all fifteen islands mount with a clean console. Two things the scan flagged that are not Proxy users, recorded so the next sweep does not chase them: `attribute` belongs to the component defined in <template id="product-attribute-quickview">, which is registered through the inDomComponents island and correctly lives outside every island; `$event` is Vue's own inline-handler argument. That leaves `vm` - the shell, referenced by name from templates - as the only thing still resolved by the fall-through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…itself Everything a Grand.Web template names now comes from somewhere explicit, so the fall-through resolves nothing on that theme. Three pieces: `vm` - the shell's own name in the templates - is layered into every island's data rather than being found on window. Islands with no declaration get it too, which is why they all go through the layering wrapper now. The globals templates genuinely call are installed on globalProperties in main.js: document, window, location, bootstrap, hideTooltip. Vue's allowed-globals list covers Math and JSON but not document or window, so `document.getElementById` in a @click was reaching them through the fall-through like everything else. hideTooltip became a declaration instead of a window assignment so it can be handed over; it stays on window for markup outside a Vue expression. `standardProductAttributes_<productId>` is registered and declared. It was the last Grand.Web holdout and no fixed-page scan would have found it - the name carries the product id, so it only appears on a product that has attributes. The partial takes its own island because a grouped product renders it once per variant, each needing its own name. The fall-through itself stays, because Theme.Modern still needs it, and now warns once per name with the island that wanted it. That instrumentation is the point: removing it while a theme still depends on it is not a soft landing - an undeclared view-model stops being silently fine and throws "Cannot read properties of undefined" out of the render function, taking the island down. I removed it first and found that out on Modern, which is why it is back. Modern's remaining list, straight from the warnings: searchbox, productreviews, vendorreviews, contactvendor, standardProductAttributes_*, plus subcatslider and localStorage from a static scan - subcatslider is built in an out-of-bundle theme script, so it needs Vue.registerViewModel, and localStorage belongs in main.js with the other globals. Verified: Default theme sweep over home, category, product, vendor, reservation and the attribute-heavy product page reports nothing at all, and changing an attribute still reprices $1,315.00 -> $1,300.00. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nings The console list from 4fe6cfa, cleared. Four of these are ordinary declarations; two are not, and neither could have been guessed from Grand.Web's shape. localStorage is not a view-model. Modern's Selectors call localStorage.setItem('catalogProdCols', ...) from a @click, so it joins document, window and location as an installed global in main.js. subcatslider is built by Content/script/catalog.js, which is not part of the bundle, so nothing in views/ can register it. It calls Vue.registerViewModel itself - the reason that was exposed on window.Vue - and the category page declares the name. Ordering holds: the theme's scripts run before app.js, and app.js is what calls Vue.shell() and mounts the islands. Two roots could not take the marker and needed a wrapper instead: - the search box is a <template id="searchBoxContainer">, and a template's children live in a DocumentFragment, so it cannot be a mount point - the marker goes on the <li> above it. Grand.Web uses a <div> here, which is why its copy looked simpler. - the reviews view starts with <template v-if> and renders a modal whose root carries :dark-theme; Vue ignores bindings on a mount point, so putting the island there would have silently killed that binding. Both are wrapped in one .product-reviews div instead. Product attributes get their own island per product, same as Grand.Web: the name carries the product id and a grouped product renders the partial once per variant. Verified on Grand.Web, which shares the bundle: localStorage is installed, the attributes island mounts, and home, category, vendor and the attribute-heavy product page report nothing. **Theme.Modern itself is not verified yet** - its views compile into the plugin DLL and the running IIS Express instance still has the old one. The fall-through is still in place, so a wrong declaration here degrades to a warning rather than a broken island; the console after a restart is the check that matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Proxy that forwarded any unresolved template identifier to `window` is gone. It was the last thing standing between a Razor template and Vue 3's ordinary scoping: with it, every global happened to work, so an island that never declared what it rendered against looked exactly like one that did. Two blocks had been rendering nothing for weeks behind that - the voice-search button and the vendor rating summary - and neither showed a symptom until a feature flag was switched on. Everything a template names now comes from one of two places: a view-model declared on its island, or an explicitly installed global (document, window, location, localStorage, bootstrap, hideTooltip). globalProperties is a plain object again, and Reservation, AxiosCart and the rest no longer leak into template scope just because they exist on window. Verified without the fall-through, both themes, after clearing the warnings it had been reporting: - Modern: category with a subcategory slider (3 slides, 12 products), product page with the review modal's :dark-theme binding still live, vendor page with its five rating stars, search, cart - every island mounts, console silent. - Default: category, product, the attribute-heavy product, vendor and cart - same. Search autocomplete returns results, add to cart still moves the counter. Doing this in one step would have been a mistake, and was: removing it before Modern was declared turned silent misses into "Cannot read properties of undefined" thrown from render, killing the islands. Restoring it with a warning per resolved name turned the remainder into a list, and the list is now empty. compat/globals.js is down to foldProps, which goes with the last root-level `props` in an out-of-bundle theme script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were dormant - nothing on the storefront triggers either today - and both
would have given a wrong answer silently rather than failing, which is the kind
that gets found by a customer.
exact_length ignored its parameter. The body only tested for non-empty, so
`exact_length:3` behaved exactly like `exact_length:1` while the rule's own
message promised "Select at least {0} option". Harmless right now because all
four call sites - customer and address attributes - pass 1. It now measures
length, with the count taken from the parameter. Checked both ways: the existing
`:1` groups behave as before (unchecked errors, checked clears), and a probe with
`:2` correctly rejects one ticked box where it used to accept it.
veeGetMessage looked its field up with document.getElementsByName(...)[0] - the
whole document, first match wins - so two fields sharing a name would answer with
each other's message. The page and the quick-view modal render the same forms, so
this is the same shape as the FormData bug in 7276256; it does not misfire only
because no *validated* name is currently duplicated. The lookup is now scoped to
the provider's own element, with the document kept as the fallback for
window.vee_getMessage, which is called from outside the bundle. A probe with two
providers sharing a name now gives each its own message; before, both showed the
first one's.
The rest of the file is left alone deliberately. It reads the DOM rather than the
bound model, which is what makes it a shim rather than idiomatic Vue 3, but it is
carefully written, documented, and frozen by roughly 400 validation tags across
49 views. Replacing it with vee-validate 4 would trade a working 319 lines for
touching all of those, which is a bad deal at this point.
Verified on the real forms: contact reports its own "Enter email"/"Enter your
name" from data-val-*, login reports "Please enter your email", a malformed
address still says "Wrong email" and .online passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deleting it in 29b13b1 was premature. Two things broke in normal use and a customer found both, not me: the address form threw 27 render errors on /account/addressadd, and the checkout's payment step threw on "Continue". The sweep that justified the removal had a hole. It matched identifiers followed by `.` or `(` - things used as objects or called as functions - so a bare identifier passed as an argument was invisible to it. That is exactly the shape of `vmorder.setDisabled(event)`, where `event` had been resolving to the legacy `window.event` through the fall-through. Nothing behind a login was swept either, which is where the address form lives. Three real fixes, all worth keeping regardless of the Proxy: - Partials/CreateOrUpdateAddress declares its view-model on its own `.edit-address row`. The name carries the field prefix and six hosts use five different ones, so declaring it in the partial is automatically right for all of them - account, checkout billing and shipping, merchandise return, and Theme.Modern's account views, which render this same file. The marker goes on that existing row rather than a new wrapper, which would sit between .row and its .col-* children and break the grid. - Checkout/Start uses `$event`, Vue's own inline-handler argument, instead of the bare `event` it inherited from Vue 2. - `vm` is installed as a global property, not only layered into island data. The popups displayPopup renders are made with Vue.createApp and are not islands, so they never got the layering; a getter, because the shell does not exist when the first apps are created. The fall-through is back in warning mode until a sweep that covers signed-in pages, both themes, and identifiers in every position comes back empty. Verified after restoring: quick view on Build your own computer reprices $1,315 -> $1,300 with a silent console, and the address form mounts with its 238 countries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Marking .edit-address as an island in eefccce put the address fields in a different Vue app from the <validation-observer> that wraps them in the host view. provide/inject does not cross app boundaries, so every <validation-provider> inside failed to register: the observer held zero fields and handleSubmit answered "valid" no matter what the form contained. Measured before the fix, on /account/addressadd with a required field emptied: providers registered 0, observer.validate() -> true. An empty required address went through. Individual fields still flagged themselves on blur, which is why this looked like it worked when I checked that the island mounted and the inputs rendered - the part that broke is only visible from the observer's side. After: 13 providers registered, validate() -> false, and on checkout pressing Continue with an empty first name keeps the step open and shows "First name is required.", "Country is required.", "State / province is required.", "City is required.". The lesson is a rule, not a one-off: an island boundary is an app boundary, so it must never be drawn between a component that provides and one that injects. The view-model this partial binds to therefore has to be declared on whichever island already contains the observer, in the host view - not here. Until that is done it resolves through the window fall-through, which is why the fall-through is still in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On a vendor page, "Be the first to review this vendor" darkened the screen and showed nothing. The modal was open - .show, display:block, one backdrop, body .modal-open - and measured 0x0, because it is rendered inside the Reviews <gn-tab> pane and an inactive pane is display:none. The backdrop is a sibling of the page, so it covers everything; the modal it belongs to has no size. The product page has the same markup and does not show the symptom only because its trigger lives inside that pane too - you cannot reach it until the tab is already open. The vendor page's trigger is in the rating summary, outside the tabs, so it hit this every time. addReview now shows the containing pane first when there is one and it is not already active. Fixed in the shared review view-model rather than in either view, because it is the modal's location that is unsafe, not the caller's: any future trigger placed outside the tabs would have hit the same thing. Pre-existing, not fallout from the island work - the only change this file has had from me is the island declaration in 4be1b8f, and the modal has been inside the tab since long before that. Verified: on the vendor page the button now activates the pane and opens a 2560x1253 modal with the form visible, one backdrop, clean console; the product page's own "Add your review" still opens its modal with the five rating stars. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by a full regression sweep rather than by looking: Account/Reviews had no
island anywhere in its ancestry, so all 39 <b-form-rating> tags stayed unknown
elements. A component tag outside every island produces no error and no output -
the page simply showed no stars, and nothing on it hinted that anything was
missing. Same family as the voice-navigation button and the vendor rating
summary; this one was never behind a feature flag, it was just never looked at.
Now 195 star icons render and no raw <b-form-rating> is left.
The island also brings the review text into Vue's compiler, and one seeded review
contains "{{ dd}}". Without protection that gets read as an expression and
disappears - a customer's own words silently edited by the framework. The text
node carries v-pre, so it stays literal; verified both together, stars present
*and* the braces still on screen.
Theme.Modern has its own copy of this view and does not need the same change: it
renders no Vue construct at all, so an island there would be overhead. Checked
rather than assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hallow I reverted this change an hour ago after grepping Theme.Modern's Reviews.cshtml for Vue constructs and finding none. The regression sweep then reported 39 unresolved <b-form-rating> tags on that exact page: the components arrive through <partial name="Partials/Rating">, which the grep never opened. Checking the view in isolation was the mistake - what matters is what the page renders. Island plus v-pre on the review text, same as Grand.Web's copy in b5fa89c. NOT verified in the browser: Modern's views compile into the plugin DLL and the running instance still holds the old one. The Grand.Web equivalent was verified (195 stars, no raw tags, "{{ dd}}" preserved), and this is the same edit to the same shape of view, but it needs a restart plus a signed-in customer to confirm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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