Theme shop tab to match printables, add dark mode support to Shopify embed

Theme shop tab to match printables, add dark mode support to Shopify embed

Shop tab layout
- Wrap Shopify embed in v-card (elevation 2) to match printables card style
- Add skeleton loaders while the Buy Button SDK initializes
- Add subtitle blurb above the category filter, consistent with printables tab

Dark mode support
- Add useTheme() / isDark computed to track the active Vuetify theme
- Convert SHOPIFY_OPTIONS to buildShopifyOptions(dark) so all SDK-rendered
  surfaces can be re-themed on toggle
- Theme product tile background + text (previously hardcoded white text
  with no background, invisible in light mode)
- Theme product modal background (previously always rendered white
  regardless of site theme)
- Theme cart/checkout drawer background, header, line items, and footer
  (previously always rendered light mode)
- Re-run initShopify() via a watcher on isDark so the embed re-renders
  when the user toggles theme
- Round button border-radius from 0px to 4px across product, modal, and
  cart buttons for visual consistency with printables cards

Bug fixes
- Add shopifyRenderToken generation guard in initShopify() to prevent a
  race condition where overlapping calls (e.g. theme settling shortly
  after mount) could leave the product grid partially rendered with
  missing images
- Move subtitle blurb above the category filter dropdown (was below)

Files changed:
  src/pages/StorePage.vue
This commit is contained in:
Daniel McGee
2026-07-01 22:05:22 -04:00
committed by GitHub
parent 441b487f35
commit 2ea5b6cf06
+42 -16
View File
@@ -24,7 +24,11 @@
<!-- Shop tab --> <!-- Shop tab -->
<v-window-item value="shop" eager> <v-window-item value="shop" eager>
<v-row justify="center" class="mt-4 mb-2"> <p class="mt-4 mb-4 text-center text-medium-emphasis">
T-shirts, stickers, yard signs, and more proceeds support the anti-surveillance movement.
</p>
<v-row justify="center" class="mb-6">
<v-col cols="12" sm="8" md="5" lg="4"> <v-col cols="12" sm="8" md="5" lg="4">
<v-select <v-select
v-model="collectionId" v-model="collectionId"
@@ -47,10 +51,6 @@
</v-col> </v-col>
</v-row> </v-row>
<p class="mb-6 text-center text-medium-emphasis">
T-shirts, stickers, yard signs, and more — proceeds support the anti-surveillance movement.
</p>
<!-- Skeleton while Shopify SDK initialises --> <!-- Skeleton while Shopify SDK initialises -->
<v-row v-if="!shopifyReady" class="mt-2"> <v-row v-if="!shopifyReady" class="mt-2">
<v-col <v-col
@@ -298,17 +298,22 @@ const COLLECTIONS: Record<string, Record<string, string>> = {
}, },
}; };
// Modal text uses a light-on-dark or dark-on-light palette depending on the // Every surface the Shopify SDK renders (product tiles, the product modal,
// active Vuetify theme. The product grid tiles always render on the SDK's own // and the cart drawer) gets an explicit background + text color pulled from
// dark card surface so their text stays white regardless of theme. // this palette, since the SDK has no awareness of the host page's theme and
// otherwise falls back to its own static (light) defaults.
function buildShopifyOptions(dark: boolean) { function buildShopifyOptions(dark: boolean) {
const modalText = dark ? '#e0e0e0' : '#4c4c4c'; const surfaceBg = dark ? '#1e1e1e' : '#ffffff';
const modalText2 = dark ? '#b0b0b0' : '#6b6b6b'; const surfaceBg2 = dark ? '#2a2a2a' : '#f5f5f5'; // footer/header accent surface
const modalText = dark ? '#e0e0e0' : '#2c2c2c';
const modalText2 = dark ? '#b0b0b0' : '#6b6b6b';
const borderColor = dark ? '#3a3a3a' : '#e0e0e0';
return { return {
product: { product: {
styles: { styles: {
product: { product: {
'background-color': surfaceBg,
'@media (min-width: 601px)': { '@media (min-width: 601px)': {
'max-width': 'calc(33% - 20px)', 'max-width': 'calc(33% - 20px)',
'margin-left': '20px', 'margin-left': '20px',
@@ -316,7 +321,7 @@ function buildShopifyOptions(dark: boolean) {
width: 'calc(33% - 20px)', width: 'calc(33% - 20px)',
}, },
}, },
title: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, title: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: modalText },
button: { button: {
'font-family': 'Raleway, sans-serif', 'font-family': 'Raleway, sans-serif',
'font-weight': 'bold', 'font-weight': 'bold',
@@ -332,10 +337,10 @@ function buildShopifyOptions(dark: boolean) {
'padding-right': '20px', 'padding-right': '20px',
}, },
quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' }, quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' },
price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: modalText },
compareAt: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: '#ffffff' }, compareAt: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: modalText2 },
unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: '#ffffff' }, unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: modalText2 },
description: { 'font-family': 'Raleway, sans-serif' }, description: { 'font-family': 'Raleway, sans-serif', color: modalText2 },
}, },
buttonDestination: 'modal', buttonDestination: 'modal',
contents: { button: false, options: false }, contents: { button: false, options: false },
@@ -354,7 +359,11 @@ function buildShopifyOptions(dark: boolean) {
modalProduct: { modalProduct: {
contents: { img: false, imgWithCarousel: true, button: false, buttonWithQuantity: true }, contents: { img: false, imgWithCarousel: true, button: false, buttonWithQuantity: true },
styles: { styles: {
product: { '@media (min-width: 601px)': { 'max-width': '100%', 'margin-left': '0px', 'margin-bottom': '0px' } }, modal: { 'background-color': surfaceBg },
product: {
'background-color': surfaceBg,
'@media (min-width: 601px)': { 'max-width': '100%', 'margin-left': '0px', 'margin-bottom': '0px' },
},
button: { button: {
'font-family': 'Raleway, sans-serif', 'font-family': 'Raleway, sans-serif',
'font-weight': 'bold', 'font-weight': 'bold',
@@ -388,6 +397,13 @@ function buildShopifyOptions(dark: boolean) {
}, },
cart: { cart: {
styles: { styles: {
cart: { 'background-color': surfaceBg },
header: { 'background-color': surfaceBg, color: modalText, 'border-color': borderColor },
lineItems: { 'background-color': surfaceBg, color: modalText, 'border-color': borderColor },
footer: { 'background-color': surfaceBg2, color: modalText, 'border-color': borderColor },
title: { color: modalText },
notice: { color: modalText2 },
currency: { color: modalText },
button: { button: {
'font-family': 'Raleway, sans-serif', 'font-family': 'Raleway, sans-serif',
'font-weight': 'bold', 'font-weight': 'bold',
@@ -493,10 +509,18 @@ declare global {
interface Window { ShopifyBuy: any } interface Window { ShopifyBuy: any }
} }
// Guards against overlapping initShopify calls. If the collection or theme
// changes again before a previous onReady() promise resolves, the stale
// callback below becomes a no-op instead of racing the newer one and
// leaving the container half-rendered (some product images missing).
let shopifyRenderToken = 0;
function initShopify(id: string) { function initShopify(id: string) {
const container = shopifyContainer.value; const container = shopifyContainer.value;
if (!container) return; if (!container) return;
const myToken = ++shopifyRenderToken;
shopifyReady.value = false; shopifyReady.value = false;
container.innerHTML = ''; container.innerHTML = '';
@@ -505,6 +529,8 @@ function initShopify(id: string) {
storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd', storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd',
}); });
window.ShopifyBuy.UI.onReady(client).then((ui: any) => { window.ShopifyBuy.UI.onReady(client).then((ui: any) => {
if (myToken !== shopifyRenderToken) return; // a newer render superseded this one
ui.createComponent('collection', { ui.createComponent('collection', {
id, id,
node: container, node: container,