create-shopify-firebase-app 2.0.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>{{APP_NAME}}</title>
7
7
  <link rel="stylesheet" href="/css/app.css">
8
+ <meta name="shopify-api-key" content="{{API_KEY}}">
8
9
  <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
10
+ <script src="https://cdn.shopify.com/shopifycloud/polaris.js"></script>
9
11
  </head>
10
12
  <body>
11
13
  <ui-nav-menu>
@@ -15,83 +17,92 @@
15
17
  <a href="/polaris">Components</a>
16
18
  </ui-nav-menu>
17
19
 
18
- <ui-title-bar title="Dashboard"></ui-title-bar>
19
-
20
- <div class="page">
20
+ <s-page heading="Dashboard">
21
21
  <!-- Welcome banner -->
22
- <div class="card" style="border-left: 4px solid var(--p-color-primary);">
23
- <h2>Welcome to {{APP_NAME}}</h2>
24
- <p>Your app is running on Firebase Hosting and embedded in the Shopify admin. Edit the files in <code>web/</code> to build your app.</p>
25
- </div>
22
+ <s-section>
23
+ <s-banner tone="info">
24
+ <s-text>Welcome to <s-text fontWeight="semibold">{{APP_NAME}}</s-text>! Your app is running on Firebase Hosting and embedded in the Shopify admin. Edit the files in <code>web/</code> to build your app.</s-text>
25
+ </s-banner>
26
+ </s-section>
26
27
 
27
28
  <!-- Store info -->
28
- <div class="card" id="shop-info">
29
- <div class="card-header">
30
- <h2>Store Information</h2>
31
- <span class="badge badge-success" id="shop-status" style="display:none;">Connected</span>
32
- </div>
33
- <div id="shop-info-content">
34
- <div class="loading-state">
35
- <div class="spinner"></div>
36
- <p>Loading store info...</p>
37
- </div>
38
- </div>
39
- </div>
29
+ <s-section heading="Store Information">
30
+ <s-box id="shop-loading" padding="large-400">
31
+ <s-stack alignItems="center"><s-spinner></s-spinner></s-stack>
32
+ </s-box>
33
+ <div id="shop-info-content" style="display:none;"></div>
34
+ </s-section>
40
35
 
41
36
  <!-- Stats row -->
42
- <div class="stats-grid" id="stats-grid">
43
- <div class="stat-card">
44
- <div class="stat-label">Products</div>
45
- <div class="stat-value" id="stat-products">--</div>
46
- </div>
47
- <div class="stat-card">
48
- <div class="stat-label">Store Plan</div>
49
- <div class="stat-value" id="stat-plan">--</div>
50
- </div>
51
- <div class="stat-card">
52
- <div class="stat-label">App Status</div>
53
- <div class="stat-value" style="color: var(--p-color-text-success);">Active</div>
54
- </div>
55
- </div>
37
+ <s-section>
38
+ <s-grid gridTemplateColumns="@container (inline-size <= 400px) 1fr, 1fr 1fr 1fr" gap="base">
39
+ <s-box border="base" borderRadius="base" padding="base">
40
+ <s-stack gap="small-200">
41
+ <s-text color="subdued">Products</s-text>
42
+ <s-text id="stat-products" variant="headingLg">--</s-text>
43
+ </s-stack>
44
+ </s-box>
45
+ <s-box border="base" borderRadius="base" padding="base">
46
+ <s-stack gap="small-200">
47
+ <s-text color="subdued">Store Plan</s-text>
48
+ <s-text id="stat-plan" variant="headingLg">--</s-text>
49
+ </s-stack>
50
+ </s-box>
51
+ <s-box border="base" borderRadius="base" padding="base">
52
+ <s-stack gap="small-200">
53
+ <s-text color="subdued">App Status</s-text>
54
+ <s-badge tone="success">Active</s-badge>
55
+ </s-stack>
56
+ </s-box>
57
+ </s-grid>
58
+ </s-section>
56
59
 
57
60
  <!-- Quick links -->
58
- <div class="card">
59
- <h2>Quick Links</h2>
60
- <div class="quick-links mt-4">
61
- <a class="quick-link" onclick="navigateTo('/products'); return false;" href="/products">
62
- <div class="quick-link-icon">&#128722;</div>
63
- <div class="quick-link-text">
64
- <h4>Products</h4>
65
- <p>Search and browse products</p>
66
- </div>
67
- </a>
68
- <a class="quick-link" onclick="navigateTo('/settings'); return false;" href="/settings">
69
- <div class="quick-link-icon">&#9881;</div>
70
- <div class="quick-link-text">
71
- <h4>Settings</h4>
72
- <p>Configure your app</p>
73
- </div>
74
- </a>
75
- <a class="quick-link" onclick="navigateTo('/polaris'); return false;" href="/polaris">
76
- <div class="quick-link-icon">&#127912;</div>
77
- <div class="quick-link-text">
78
- <h4>Components</h4>
79
- <p>UI component reference</p>
80
- </div>
81
- </a>
82
- </div>
83
- </div>
61
+ <s-section heading="Quick Links">
62
+ <s-grid gridTemplateColumns="@container (inline-size <= 480px) 1fr, 1fr 1fr 1fr" gap="base">
63
+ <s-box border="base" borderRadius="base" padding="base">
64
+ <s-clickable href="/products">
65
+ <s-stack gap="small-200">
66
+ <s-text variant="headingSm">Products</s-text>
67
+ <s-text color="subdued">Search and browse products</s-text>
68
+ </s-stack>
69
+ </s-clickable>
70
+ </s-box>
71
+ <s-box border="base" borderRadius="base" padding="base">
72
+ <s-clickable href="/settings">
73
+ <s-stack gap="small-200">
74
+ <s-text variant="headingSm">Settings</s-text>
75
+ <s-text color="subdued">Configure your app</s-text>
76
+ </s-stack>
77
+ </s-clickable>
78
+ </s-box>
79
+ <s-box border="base" borderRadius="base" padding="base">
80
+ <s-clickable href="/polaris">
81
+ <s-stack gap="small-200">
82
+ <s-text variant="headingSm">Components</s-text>
83
+ <s-text color="subdued">Polaris UI reference</s-text>
84
+ </s-stack>
85
+ </s-clickable>
86
+ </s-box>
87
+ </s-grid>
88
+ </s-section>
84
89
 
85
90
  <!-- Quick actions -->
86
- <div class="card">
87
- <h2>Quick Actions</h2>
88
- <p>Test your API connection and App Bridge features.</p>
89
- <div class="btn-group mt-4">
90
- <button class="btn btn-primary" onclick="testApiConnection()">Test API Connection</button>
91
- <button class="btn" onclick="showToast('Everything is working!')">Test Toast</button>
92
- </div>
93
- </div>
94
- </div>
91
+ <s-section heading="Quick Actions">
92
+ <s-text color="subdued">Test your API connection and App Bridge features.</s-text>
93
+ <s-stack direction="inline" gap="base" style="margin-top: 12px;">
94
+ <s-button variant="primary" onclick="testApiConnection()">Test API Connection</s-button>
95
+ <s-button onclick="showToast('Everything is working!')">Test Toast</s-button>
96
+ </s-stack>
97
+ </s-section>
98
+
99
+ <!-- Footer -->
100
+ <s-section>
101
+ <s-stack alignItems="center">
102
+ <s-text color="subdued">Built with Shopify App Bridge + Firebase Hosting. Edit <code>web/</code> to customize.</s-text>
103
+ </s-stack>
104
+ </s-section>
105
+ </s-page>
95
106
 
96
107
  <script src="/js/app.js"></script>
97
108
  <script src="/js/pages/home.js"></script>
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * App Bridge initialization and shared utilities
3
- * Loaded on every page -- provides apiFetch, showToast, navigateTo, and helpers.
3
+ * Loaded on every page -- provides apiFetch, showToast, and helpers.
4
4
  *
5
5
  * In embedded admin, window.shopify is injected by the App Bridge CDN script.
6
6
  * Outside embedded admin (direct URL visit), graceful fallbacks are used.
@@ -9,15 +9,37 @@
9
9
  (function () {
10
10
  "use strict";
11
11
 
12
- // ── Query params provided by Shopify when loading the app ───────
12
+ // ── Shop context ────────────────────────────────────────────────
13
+ // App Bridge is the source of truth: in the embedded admin the CDN
14
+ // script injects window.shopify, and window.shopify.config carries
15
+ // { apiKey, shop, host, locale } on EVERY page load.
16
+ //
17
+ // The ?shop=/?host= query params are only present on the first load --
18
+ // the <ui-nav-menu> links are plain hrefs with no query string -- so
19
+ // they are used purely as a fallback for viewing a page outside the
20
+ // embedded admin (e.g. hitting the Hosting URL directly).
13
21
  var params = new URLSearchParams(window.location.search);
14
- var shop = params.get("shop") || "";
15
- var host = params.get("host") || "";
16
22
 
17
- // Shared state accessible across pages
23
+ function appBridgeConfig() {
24
+ return (window.shopify && window.shopify.config) || null;
25
+ }
26
+
27
+ // Shared state accessible across pages.
28
+ // shop/host are getters, not fixed values, so they resolve at read time
29
+ // -- correct on every page and independent of script load order.
18
30
  window.__app = {
19
- shop: shop,
20
- host: host,
31
+ get shop() {
32
+ var cfg = appBridgeConfig();
33
+ if (cfg && cfg.shop) return cfg.shop;
34
+ // Query params are user-controlled -- validate before trusting.
35
+ var rawShop = params.get("shop") || "";
36
+ return /^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com$/.test(rawShop) ? rawShop : "";
37
+ },
38
+ get host() {
39
+ var cfg = appBridgeConfig();
40
+ if (cfg && cfg.host) return cfg.host;
41
+ return params.get("host") || "";
42
+ },
21
43
  ready: false,
22
44
  };
23
45
 
@@ -92,50 +114,46 @@
92
114
  }, 4000);
93
115
  };
94
116
 
95
- // ── Navigation helper ───────────────────────────────────────────
96
- // Preserves shop/host query params when navigating between pages.
97
- window.navigateTo = function navigateTo(path) {
98
- var qs = new URLSearchParams();
99
- if (window.__app.shop) qs.set("shop", window.__app.shop);
100
- if (window.__app.host) qs.set("host", window.__app.host);
101
- var search = qs.toString();
102
- window.location.href = path + (search ? "?" + search : "");
103
- };
117
+ // ── Navigation ──────────────────────────────────────────────────
118
+ // No helper needed: App Bridge intercepts in-app navigation, so plain
119
+ // <a href="/products"> links (see <ui-nav-menu>) and <s-clickable href>
120
+ // are the supported way to move between pages. Do NOT hand-append
121
+ // shop/host to URLs -- that is the legacy pattern, and window.shopify
122
+ // provides the shop context on every page (see window.__app above).
104
123
 
105
- // ── Loading state helpers ───────────────────────────────────────
124
+ // ── Loading state helpers (Polaris Web Components) ──────────────
106
125
  window.showLoading = function showLoading(containerId) {
107
126
  var el = document.getElementById(containerId);
108
127
  if (!el) return;
109
128
  el.innerHTML =
110
- '<div class="loading-state">' +
111
- '<div class="spinner"></div>' +
112
- "<p>Loading\u2026</p>" +
113
- "</div>";
129
+ '<s-box padding="large-400">' +
130
+ '<s-stack alignItems="center" gap="base">' +
131
+ "<s-spinner></s-spinner>" +
132
+ '<s-text color="subdued">Loading\u2026</s-text>' +
133
+ "</s-stack></s-box>";
114
134
  };
115
135
 
116
136
  window.hideLoading = function hideLoading(containerId) {
117
137
  var el = document.getElementById(containerId);
118
138
  if (!el) return;
119
- var spinner = el.querySelector(".loading-state");
120
- if (spinner) spinner.remove();
139
+ el.innerHTML = "";
121
140
  };
122
141
 
123
- // ── Error rendering helper ──────────────────────────────────────
142
+ // ── Error rendering helper (Polaris Web Components) ─────────────
124
143
  window.showError = function showError(containerId, message) {
125
144
  var el = document.getElementById(containerId);
126
145
  if (!el) return;
127
146
  el.innerHTML =
128
- '<div class="banner banner-critical">' +
129
- '<div class="banner-icon">&#9888;</div>' +
130
- '<div class="banner-content">' +
131
- "<p><strong>Something went wrong</strong></p>" +
132
- "<p>" + escapeHtml(message) + "</p>" +
133
- "</div></div>";
147
+ '<s-banner tone="critical">' +
148
+ "<s-text>" +
149
+ "<s-text fontWeight=\"semibold\">Something went wrong</s-text> " +
150
+ escapeHtml(message) +
151
+ "</s-text></s-banner>";
134
152
  };
135
153
 
136
154
  // ── Format helpers ──────────────────────────────────────────────
137
155
  window.formatCurrency = function formatCurrency(amount, currency) {
138
- return new Intl.NumberFormat("en-US", {
156
+ return new Intl.NumberFormat(undefined, {
139
157
  style: "currency",
140
158
  currency: currency || "USD",
141
159
  }).format(amount);
@@ -143,7 +161,7 @@
143
161
 
144
162
  window.formatDate = function formatDate(dateStr) {
145
163
  if (!dateStr) return "--";
146
- return new Date(dateStr).toLocaleDateString("en-US", {
164
+ return new Date(dateStr).toLocaleDateString(undefined, {
147
165
  year: "numeric",
148
166
  month: "short",
149
167
  day: "numeric",
@@ -171,7 +189,16 @@
171
189
  }
172
190
  window.escapeHtml = escapeHtml;
173
191
 
192
+ function escapeAttr(str) {
193
+ return String(str)
194
+ .replace(/&/g, "&amp;")
195
+ .replace(/"/g, "&quot;")
196
+ .replace(/'/g, "&#39;")
197
+ .replace(/</g, "&lt;")
198
+ .replace(/>/g, "&gt;");
199
+ }
200
+ window.escapeAttr = escapeAttr;
201
+
174
202
  // ── Mark ready ──────────────────────────────────────────────────
175
203
  window.__app.ready = true;
176
- console.log("[app.js] Initialized", { shop: shop, host: host });
177
204
  })();
@@ -1,43 +1,54 @@
1
1
  /**
2
2
  * Dashboard (home) page logic
3
- * Fetches shop info and renders store details + stats.
3
+ * Fetches shop info and renders store details + stats using Polaris Web Components.
4
4
  */
5
5
 
6
6
  (function () {
7
7
  "use strict";
8
8
 
9
9
  // ── Load shop data on page ready ────────────────────────────────
10
- document.addEventListener("DOMContentLoaded", loadDashboard);
10
+ document.addEventListener("DOMContentLoaded", function () {
11
+ if (typeof apiFetch !== "function") {
12
+ var loading = document.getElementById("shop-loading");
13
+ if (loading) {
14
+ loading.innerHTML =
15
+ '<s-banner tone="critical"><s-text><s-text fontWeight="semibold">App failed to initialize</s-text> Core scripts did not load. Please refresh the page.</s-text></s-banner>';
16
+ }
17
+ return;
18
+ }
19
+ loadDashboard();
20
+ });
11
21
 
12
22
  async function loadDashboard() {
13
- var container = document.getElementById("shop-info-content");
14
- var statusBadge = document.getElementById("shop-status");
23
+ var loading = document.getElementById("shop-loading");
24
+ var content = document.getElementById("shop-info-content");
15
25
 
16
26
  try {
17
27
  var data = await apiFetch("/api/shop");
18
28
  var shop = data.shop || data;
19
29
 
20
30
  // Render store info
21
- renderShopInfo(container, shop);
31
+ renderShopInfo(content, shop);
22
32
 
23
- // Show connected badge
24
- if (statusBadge) statusBadge.style.display = "";
33
+ // Hide loading, show content
34
+ if (loading) loading.style.display = "none";
35
+ content.style.display = "";
25
36
 
26
37
  // Update stats
27
38
  updateStats(shop);
28
39
  } catch (err) {
29
- container.innerHTML =
30
- '<div class="banner banner-critical">' +
31
- '<div class="banner-icon">&#9888;</div>' +
32
- '<div class="banner-content">' +
33
- "<p><strong>Could not load store information</strong></p>" +
34
- "<p>" + escapeHtml(err.message) + "</p>" +
35
- "</div></div>";
40
+ if (loading) {
41
+ loading.innerHTML =
42
+ '<s-banner tone="critical">' +
43
+ "<s-text>" +
44
+ "<s-text fontWeight=\"semibold\">Could not load store information</s-text> " +
45
+ escapeHtml(err.message) +
46
+ "</s-text></s-banner>";
47
+ }
36
48
  }
37
49
  }
38
50
 
39
51
  // ── Render shop info list ───────────────────────────────────────
40
- // The /api/shop endpoint returns GraphQL field names (camelCase).
41
52
  function renderShopInfo(container, shop) {
42
53
  var fields = [
43
54
  { label: "Store name", value: shop.name || "--" },
@@ -49,15 +60,16 @@
49
60
  { label: "Shopify domain", value: shop.myshopifyDomain || "--" },
50
61
  ];
51
62
 
52
- var html = '<ul class="info-list">';
63
+ var html = '<s-box border="base" borderRadius="base" padding="base">';
64
+ html += '<s-stack gap="small-200">';
53
65
  for (var i = 0; i < fields.length; i++) {
54
66
  html +=
55
- "<li>" +
56
- '<span class="info-list-label">' + escapeHtml(fields[i].label) + "</span>" +
57
- '<span class="info-list-value">' + escapeHtml(String(fields[i].value)) + "</span>" +
58
- "</li>";
67
+ '<s-grid gridTemplateColumns="1fr 1fr" gap="small-200">' +
68
+ '<s-text color="subdued">' + escapeHtml(fields[i].label) + "</s-text>" +
69
+ "<s-text>" + escapeHtml(String(fields[i].value)) + "</s-text>" +
70
+ "</s-grid>";
59
71
  }
60
- html += "</ul>";
72
+ html += "</s-stack></s-box>";
61
73
 
62
74
  container.innerHTML = html;
63
75
  }
@@ -68,7 +80,6 @@
68
80
  var planEl = document.getElementById("stat-plan");
69
81
 
70
82
  if (productsEl) {
71
- // GraphQL returns productCount: { count } via alias
72
83
  var count = shop.productCount && shop.productCount.count;
73
84
  productsEl.textContent = count !== undefined && count !== null ? String(count) : "--";
74
85
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Polaris component demo page logic
3
- * Copy-to-clipboard, live demos, collapsible sections, and interactive examples.
2
+ * Polaris Web Components reference page logic
3
+ * Copy-to-clipboard, resource picker demo, and interactive examples.
4
4
  */
5
5
 
6
6
  (function () {
@@ -8,7 +8,6 @@
8
8
 
9
9
  // ── Copy to clipboard ──────────────────────────────────────────
10
10
  window.copyCode = function (button) {
11
- // Find the <pre><code> sibling
12
11
  var container = button.closest(".demo-code") || button.closest(".code-block");
13
12
  if (!container) return;
14
13
 
@@ -17,7 +16,6 @@
17
16
 
18
17
  var text = codeEl.textContent;
19
18
 
20
- // Use clipboard API when available
21
19
  if (navigator.clipboard && navigator.clipboard.writeText) {
22
20
  navigator.clipboard.writeText(text).then(function () {
23
21
  showCopiedFeedback(button);
@@ -55,64 +53,6 @@
55
53
  }, 2000);
56
54
  }
57
55
 
58
- // ── Banner reset ───────────────────────────────────────────────
59
- window.resetBanners = function () {
60
- var preview = document.getElementById("banners-preview");
61
- if (!preview) return;
62
-
63
- // Remove existing banners
64
- var existing = preview.querySelectorAll(".banner");
65
- for (var i = 0; i < existing.length; i++) {
66
- existing[i].remove();
67
- }
68
-
69
- // Re-insert banners before the reset button
70
- var resetBtn = preview.querySelector("button");
71
-
72
- var banners = [
73
- {
74
- cls: "banner-info",
75
- icon: "&#8505;",
76
- title: "Information",
77
- text: "This is an informational banner. Use it for tips or neutral messages.",
78
- },
79
- {
80
- cls: "banner-success",
81
- icon: "&#10003;",
82
- title: "Success",
83
- text: "The operation completed successfully. Your changes have been saved.",
84
- },
85
- {
86
- cls: "banner-warning",
87
- icon: "&#9888;",
88
- title: "Warning",
89
- text: "This action may have unintended consequences. Please review before proceeding.",
90
- },
91
- {
92
- cls: "banner-critical",
93
- icon: "&#10006;",
94
- title: "Error",
95
- text: "Something went wrong. Please try again or contact support.",
96
- },
97
- ];
98
-
99
- for (var j = 0; j < banners.length; j++) {
100
- var b = banners[j];
101
- var div = document.createElement("div");
102
- div.className = "banner " + b.cls;
103
- div.innerHTML =
104
- '<div class="banner-icon">' + b.icon + "</div>" +
105
- '<div class="banner-content">' +
106
- "<p><strong>" + b.title + "</strong></p>" +
107
- "<p>" + b.text + "</p>" +
108
- "</div>" +
109
- '<button class="banner-dismiss" onclick="this.closest(\'.banner\').remove()">&times;</button>';
110
- preview.insertBefore(div, resetBtn);
111
- }
112
-
113
- showToast("Banners restored");
114
- };
115
-
116
56
  // ── Confirmation delete demo ───────────────────────────────────
117
57
  window.demoConfirmDelete = function () {
118
58
  var modal = document.getElementById("demo-modal-confirm");
@@ -127,13 +67,12 @@
127
67
  var resultDiv = document.getElementById("picker-demo-result");
128
68
  if (resultDiv) {
129
69
  resultDiv.innerHTML =
130
- '<div class="banner banner-warning" style="margin-bottom:0;">' +
131
- '<div class="banner-icon">&#9888;</div>' +
132
- '<div class="banner-content">' +
133
- "<p><strong>Not available</strong></p>" +
134
- "<p>The Resource Picker requires the app to be loaded inside the Shopify admin iframe. " +
135
- "Install the app on a Shopify store to test this feature.</p>" +
136
- "</div></div>";
70
+ '<s-banner tone="warning">' +
71
+ "<s-text>" +
72
+ "<s-text fontWeight=\"semibold\">Not available</s-text> " +
73
+ "The Resource Picker requires the app to be loaded inside the Shopify admin iframe. " +
74
+ "Install the app on a Shopify store to test this feature." +
75
+ "</s-text></s-banner>";
137
76
  }
138
77
  return;
139
78
  }
@@ -143,20 +82,20 @@
143
82
  var resultDiv = document.getElementById("picker-demo-result");
144
83
 
145
84
  if (!selected || selected.length === 0) {
146
- if (resultDiv) resultDiv.innerHTML = '<p class="text-secondary">Selection cancelled.</p>';
85
+ if (resultDiv) resultDiv.innerHTML = '<s-text color="subdued">Selection cancelled.</s-text>';
147
86
  return;
148
87
  }
149
88
 
150
- var html = '<div class="card" style="margin-bottom:0;">';
151
- html += '<h3 class="mb-3">Selected ' + selected.length + " product(s)</h3>";
152
- html += '<ul class="info-list">';
89
+ var html = '<s-box border="base" borderRadius="base" padding="base">';
90
+ html += '<s-stack gap="base">';
91
+ html += '<s-text variant="headingSm">Selected ' + selected.length + " product(s)</s-text>";
153
92
  for (var i = 0; i < selected.length; i++) {
154
- html += "<li>" +
155
- '<span class="font-semibold">' + escapeHtml(selected[i].title || "Untitled") + "</span>" +
156
- '<span class="badge badge-success">Selected</span>' +
157
- "</li>";
93
+ html += '<s-stack direction="inline" gap="base" alignItems="center">';
94
+ html += '<s-text fontWeight="semibold">' + escapeHtml(selected[i].title || "Untitled") + "</s-text>";
95
+ html += '<s-badge tone="success">Selected</s-badge>';
96
+ html += "</s-stack>";
158
97
  }
159
- html += "</ul></div>";
98
+ html += "</s-stack></s-box>";
160
99
  if (resultDiv) resultDiv.innerHTML = html;
161
100
  } catch (err) {
162
101
  showToast("Resource picker error: " + err.message, true);
@@ -176,13 +115,16 @@
176
115
 
177
116
  // ── Smooth scroll for anchor links ─────────────────────────────
178
117
  document.addEventListener("DOMContentLoaded", function () {
179
- var anchors = document.querySelectorAll('a[href^="#section-"]');
118
+ var anchors = document.querySelectorAll('a[href^="#section-"], s-button[href^="#section-"]');
180
119
  for (var i = 0; i < anchors.length; i++) {
181
120
  anchors[i].addEventListener("click", function (e) {
182
- e.preventDefault();
183
- var target = document.querySelector(this.getAttribute("href"));
184
- if (target) {
185
- target.scrollIntoView({ behavior: "smooth", block: "start" });
121
+ var href = this.getAttribute("href");
122
+ if (href && href.charAt(0) === "#") {
123
+ e.preventDefault();
124
+ var target = document.querySelector(href);
125
+ if (target) {
126
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
127
+ }
186
128
  }
187
129
  });
188
130
  }