@jamwidgets/astro 0.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.
@@ -0,0 +1,317 @@
1
+ ---
2
+ /**
3
+ * JamWidgets Reactions Component
4
+ *
5
+ * Displays reaction buttons (like, clap, etc.) for a page.
6
+ * Customize with CSS custom properties.
7
+ *
8
+ * @example
9
+ * <Reactions
10
+ * siteKey={import.meta.env.JAMWIDGETS_SITE_KEY}
11
+ * pageId={Astro.url.pathname}
12
+ * reactions={['like', 'love', 'clap']}
13
+ * />
14
+ */
15
+
16
+ interface Props {
17
+ /** Your site key (required) */
18
+ siteKey: string;
19
+ /** Unique identifier for this page (e.g., slug or URL path) */
20
+ pageId: string;
21
+ /** Base URL of your JamWidgets instance (default: 'https://jamwidgets.com') */
22
+ endpoint?: string;
23
+ /** Reaction types to show (default: ['like']) */
24
+ reactions?: string[];
25
+ /** Custom icons for reaction types (emoji or text) */
26
+ icons?: Record<string, string>;
27
+ /** Theme preset: 'light' (default), 'dark', or 'auto' (uses prefers-color-scheme) */
28
+ theme?: "light" | "dark" | "auto";
29
+ /** CSS class to add to the container */
30
+ class?: string;
31
+ }
32
+
33
+ const DEFAULT_ENDPOINT = "https://jamwidgets.com";
34
+ const API_PATH = "/api/v1";
35
+
36
+ const defaultIcons: Record<string, string> = {
37
+ like: "\u{1F44D}",
38
+ love: "\u{2764}\u{FE0F}",
39
+ clap: "\u{1F44F}",
40
+ fire: "\u{1F525}",
41
+ think: "\u{1F914}",
42
+ sad: "\u{1F622}",
43
+ laugh: "\u{1F602}",
44
+ };
45
+
46
+ const {
47
+ siteKey,
48
+ pageId,
49
+ endpoint = DEFAULT_ENDPOINT,
50
+ reactions = ["like"],
51
+ icons = {},
52
+ theme = "light",
53
+ class: className = "",
54
+ } = Astro.props;
55
+
56
+ // Merge custom icons with defaults
57
+ const mergedIcons = { ...defaultIcons, ...icons };
58
+
59
+ function getIcon(type: string): string {
60
+ return mergedIcons[type] || type;
61
+ }
62
+
63
+ // Build the API URL
64
+ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
65
+ ---
66
+
67
+ <div
68
+ class:list={["jamwidgets-reactions", `jamwidgets-theme-${theme}`, className]}
69
+ data-jamwidgets-reactions
70
+ data-endpoint={baseUrl}
71
+ data-site-key={siteKey}
72
+ data-page-id={pageId}
73
+ >
74
+ {
75
+ reactions.map((reaction) => (
76
+ <button
77
+ type="button"
78
+ class="jamwidgets-reaction-btn"
79
+ data-reaction-type={reaction}
80
+ title={reaction}
81
+ >
82
+ <span class="jamwidgets-reaction-icon">{getIcon(reaction)}</span>
83
+ <span class="jamwidgets-reaction-count">0</span>
84
+ </button>
85
+ ))
86
+ }
87
+ </div>
88
+
89
+ <script>
90
+ // Get or create a visitor ID for anonymous users
91
+ const VISITOR_KEY = "jamwidgets_visitor_id";
92
+ function getVisitorId(): string {
93
+ let id = localStorage.getItem(VISITOR_KEY);
94
+ if (!id) {
95
+ id = crypto.randomUUID();
96
+ localStorage.setItem(VISITOR_KEY, id);
97
+ }
98
+ return id;
99
+ }
100
+
101
+ document.querySelectorAll("[data-jamwidgets-reactions]").forEach((container) => {
102
+ const endpoint = (container as HTMLElement).dataset.endpoint;
103
+ const siteKey = (container as HTMLElement).dataset.siteKey;
104
+ const pageId = (container as HTMLElement).dataset.pageId;
105
+
106
+ if (!endpoint || !siteKey || !pageId) return;
107
+
108
+ const visitorId = getVisitorId();
109
+ const headers = {
110
+ "X-JamWidgets-Key": siteKey,
111
+ "X-JamWidgets-Visitor": visitorId,
112
+ };
113
+
114
+ // Track which reactions the user has made (stored in localStorage)
115
+ const storageKey = `jamwidgets-reactions-${pageId}`;
116
+ const userReactions = new Set<string>(
117
+ JSON.parse(localStorage.getItem(storageKey) || "[]")
118
+ );
119
+
120
+ // Apply initial "reacted" state
121
+ container.querySelectorAll(".jamwidgets-reaction-btn").forEach((btn) => {
122
+ const type = (btn as HTMLElement).dataset.reactionType;
123
+ if (type && userReactions.has(type)) {
124
+ btn.classList.add("jamwidgets-reacted");
125
+ }
126
+ });
127
+
128
+ async function loadReactions() {
129
+ try {
130
+ const response = await fetch(
131
+ `${endpoint}/reactions/${encodeURIComponent(pageId!)}`,
132
+ { headers },
133
+ );
134
+ if (!response.ok) return;
135
+ const data = await response.json();
136
+ const result = data.reaction_counts_with_user;
137
+ const counts = result?.counts || {};
138
+ const serverUserReactions = result?.userReactions || [];
139
+
140
+ // Sync with server's user reactions
141
+ serverUserReactions.forEach((type: string) => userReactions.add(type));
142
+ localStorage.setItem(storageKey, JSON.stringify([...userReactions]));
143
+
144
+ container.querySelectorAll(".jamwidgets-reaction-btn").forEach((btn) => {
145
+ const type = (btn as HTMLElement).dataset.reactionType;
146
+ const countEl = btn.querySelector(".jamwidgets-reaction-count");
147
+ if (type) {
148
+ if (countEl) {
149
+ countEl.textContent = String(counts[type] || 0);
150
+ }
151
+ // Update visual state based on server data
152
+ if (userReactions.has(type)) {
153
+ btn.classList.add("jamwidgets-reacted");
154
+ } else {
155
+ btn.classList.remove("jamwidgets-reacted");
156
+ }
157
+ }
158
+ });
159
+ } catch (e) {
160
+ console.error("Failed to load reactions:", e);
161
+ }
162
+ }
163
+
164
+ container.querySelectorAll(".jamwidgets-reaction-btn").forEach((btn) => {
165
+ btn.addEventListener("click", async () => {
166
+ const type = (btn as HTMLElement).dataset.reactionType;
167
+ if (!type) return;
168
+
169
+ const isReacted = userReactions.has(type);
170
+ btn.classList.add("jamwidgets-loading");
171
+
172
+ try {
173
+ const response = await fetch(
174
+ `${endpoint}/reactions/${encodeURIComponent(pageId)}`,
175
+ {
176
+ method: isReacted ? "DELETE" : "POST",
177
+ headers: {
178
+ ...headers,
179
+ "Content-Type": "application/json",
180
+ },
181
+ body: JSON.stringify({ reactionType: type }),
182
+ },
183
+ );
184
+
185
+ if (!response.ok) throw new Error(isReacted ? "Failed to remove reaction" : "Failed to add reaction");
186
+
187
+ const data = await response.json();
188
+ const countEl = btn.querySelector(".jamwidgets-reaction-count");
189
+ if (countEl) {
190
+ countEl.textContent = String(data.reaction?.count || 0);
191
+ }
192
+
193
+ if (isReacted) {
194
+ // Remove reaction
195
+ btn.classList.remove("jamwidgets-reacted");
196
+ userReactions.delete(type);
197
+ container.dispatchEvent(
198
+ new CustomEvent("jamwidgets:reaction-removed", {
199
+ detail: data.reaction,
200
+ }),
201
+ );
202
+ } else {
203
+ // Add reaction
204
+ btn.classList.add("jamwidgets-reacted");
205
+ userReactions.add(type);
206
+ container.dispatchEvent(
207
+ new CustomEvent("jamwidgets:reaction-added", {
208
+ detail: data.reaction,
209
+ }),
210
+ );
211
+ }
212
+ localStorage.setItem(storageKey, JSON.stringify([...userReactions]));
213
+ } catch (e) {
214
+ console.error(isReacted ? "Failed to remove reaction:" : "Failed to add reaction:", e);
215
+ } finally {
216
+ btn.classList.remove("jamwidgets-loading");
217
+ }
218
+ });
219
+ });
220
+
221
+ loadReactions();
222
+ });
223
+ </script>
224
+
225
+ <style is:global>
226
+ .jamwidgets-reactions {
227
+ --jamwidgets-border-color: #e5e7eb;
228
+ --jamwidgets-button-bg: white;
229
+ --jamwidgets-hover-bg: #f3f4f6;
230
+ --jamwidgets-hover-border: #d1d5db;
231
+ --jamwidgets-active-bg: #dbeafe;
232
+ --jamwidgets-active-border: #93c5fd;
233
+ --jamwidgets-active-hover-bg: #bfdbfe;
234
+ --jamwidgets-count-color: #6b7280;
235
+ --jamwidgets-active-text: #1d4ed8;
236
+
237
+ display: flex;
238
+ gap: 0.5rem;
239
+ flex-wrap: wrap;
240
+ }
241
+
242
+ /* Dark theme */
243
+ .jamwidgets-reactions.jamwidgets-theme-dark {
244
+ --jamwidgets-border-color: #374151;
245
+ --jamwidgets-button-bg: transparent;
246
+ --jamwidgets-hover-bg: #374151;
247
+ --jamwidgets-hover-border: #4b5563;
248
+ --jamwidgets-active-bg: rgba(59, 130, 246, 0.2);
249
+ --jamwidgets-active-border: #3b82f6;
250
+ --jamwidgets-active-hover-bg: rgba(59, 130, 246, 0.3);
251
+ --jamwidgets-count-color: #9ca3af;
252
+ --jamwidgets-active-text: #60a5fa;
253
+ }
254
+
255
+ /* Auto theme - follows prefers-color-scheme */
256
+ @media (prefers-color-scheme: dark) {
257
+ .jamwidgets-reactions.jamwidgets-theme-auto {
258
+ --jamwidgets-border-color: #374151;
259
+ --jamwidgets-button-bg: transparent;
260
+ --jamwidgets-hover-bg: #374151;
261
+ --jamwidgets-hover-border: #4b5563;
262
+ --jamwidgets-active-bg: rgba(59, 130, 246, 0.2);
263
+ --jamwidgets-active-border: #3b82f6;
264
+ --jamwidgets-active-hover-bg: rgba(59, 130, 246, 0.3);
265
+ --jamwidgets-count-color: #9ca3af;
266
+ --jamwidgets-active-text: #60a5fa;
267
+ }
268
+ }
269
+
270
+ .jamwidgets-reaction-btn {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ gap: 0.375rem;
274
+ padding: 0.5rem 0.75rem;
275
+ border: 1px solid var(--jamwidgets-border-color);
276
+ border-radius: 9999px;
277
+ background: var(--jamwidgets-button-bg);
278
+ cursor: pointer;
279
+ transition: all 0.15s ease;
280
+ font-family: inherit;
281
+ font-size: 0.875rem;
282
+ }
283
+
284
+ .jamwidgets-reaction-btn:hover:not(.jamwidgets-reacted) {
285
+ background: var(--jamwidgets-hover-bg);
286
+ border-color: var(--jamwidgets-hover-border);
287
+ }
288
+
289
+ .jamwidgets-reaction-btn.jamwidgets-reacted {
290
+ background: var(--jamwidgets-active-bg);
291
+ border-color: var(--jamwidgets-active-border);
292
+ }
293
+
294
+ .jamwidgets-reaction-btn.jamwidgets-reacted:hover {
295
+ background: var(--jamwidgets-active-hover-bg);
296
+ }
297
+
298
+ .jamwidgets-reaction-btn.jamwidgets-loading {
299
+ opacity: 0.6;
300
+ cursor: wait;
301
+ }
302
+
303
+ .jamwidgets-reaction-icon {
304
+ font-size: 1.125rem;
305
+ line-height: 1;
306
+ }
307
+
308
+ .jamwidgets-reaction-count {
309
+ color: var(--jamwidgets-count-color);
310
+ font-weight: 500;
311
+ min-width: 1ch;
312
+ }
313
+
314
+ .jamwidgets-reacted .jamwidgets-reaction-count {
315
+ color: var(--jamwidgets-active-text);
316
+ }
317
+ </style>
@@ -0,0 +1,314 @@
1
+ ---
2
+ /**
3
+ * JamWidgets Subscribe Component
4
+ *
5
+ * A subscription form for email updates. Implements double opt-in -
6
+ * users receive a confirmation email and must click to confirm.
7
+ *
8
+ * @example
9
+ * <Subscribe siteKey={import.meta.env.JAMWIDGETS_SITE_KEY} />
10
+ *
11
+ * @example With custom styling
12
+ * <Subscribe
13
+ * siteKey={import.meta.env.JAMWIDGETS_SITE_KEY}
14
+ * buttonText="Get Updates"
15
+ * placeholder="Enter your email"
16
+ * successMessage="Check your inbox!"
17
+ * />
18
+ */
19
+
20
+ interface Props {
21
+ /** Your site key (required) */
22
+ siteKey: string;
23
+ /** Base URL of your JamWidgets instance (default: 'https://jamwidgets.com') */
24
+ endpoint?: string;
25
+ /** Submit button text (default: 'Subscribe') */
26
+ buttonText?: string;
27
+ /** Email input placeholder (default: 'your@email.com') */
28
+ placeholder?: string;
29
+ /** Success message override (default: uses server response) */
30
+ successMessage?: string;
31
+ /** Theme preset: 'light' (default), 'dark', or 'auto' (uses prefers-color-scheme) */
32
+ theme?: "light" | "dark" | "auto";
33
+ /** CSS class to add to the form */
34
+ class?: string;
35
+ }
36
+
37
+ const DEFAULT_ENDPOINT = "https://jamwidgets.com";
38
+ const API_PATH = "/api/v1";
39
+
40
+ const {
41
+ siteKey,
42
+ endpoint = DEFAULT_ENDPOINT,
43
+ buttonText = "Subscribe",
44
+ placeholder = "your@email.com",
45
+ successMessage,
46
+ theme = "light",
47
+ class: className = "",
48
+ } = Astro.props;
49
+
50
+ // Build the API URL
51
+ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
52
+ ---
53
+
54
+ <form
55
+ class:list={["jamwidgets-subscribe", `jamwidgets-theme-${theme}`, className]}
56
+ data-jamwidgets-subscribe
57
+ data-endpoint={baseUrl}
58
+ data-site-key={siteKey}
59
+ data-success-message={successMessage}
60
+ >
61
+ <div class="jamwidgets-subscribe-input-group">
62
+ <input
63
+ type="email"
64
+ name="email"
65
+ required
66
+ placeholder={placeholder}
67
+ class="jamwidgets-subscribe-input"
68
+ autocomplete="email"
69
+ />
70
+ <button type="submit" class="jamwidgets-subscribe-button">
71
+ {buttonText}
72
+ </button>
73
+ </div>
74
+
75
+ <!-- Honeypot field for spam protection (hidden from users, catches bots) -->
76
+ <div style="position: absolute; left: -9999px;" aria-hidden="true">
77
+ <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" />
78
+ </div>
79
+
80
+ <!-- Success/Error message container (hidden by default) -->
81
+ <div class="jamwidgets-subscribe-message" style="display: none;" aria-live="polite"></div>
82
+ </form>
83
+
84
+ <script>
85
+ interface SubscribeResponse {
86
+ success: boolean;
87
+ message: string;
88
+ }
89
+
90
+ // Get or create a visitor ID for anonymous users
91
+ const VISITOR_KEY = "jamwidgets_visitor_id";
92
+ function getVisitorId(): string {
93
+ let id = localStorage.getItem(VISITOR_KEY);
94
+ if (!id) {
95
+ id = crypto.randomUUID();
96
+ localStorage.setItem(VISITOR_KEY, id);
97
+ }
98
+ return id;
99
+ }
100
+
101
+ document.querySelectorAll("[data-jamwidgets-subscribe]").forEach((form) => {
102
+ form.addEventListener("submit", async (e) => {
103
+ e.preventDefault();
104
+
105
+ const formEl = e.target as HTMLFormElement;
106
+ const endpoint = formEl.dataset.endpoint;
107
+ const siteKey = formEl.dataset.siteKey;
108
+ const customSuccessMessage = formEl.dataset.successMessage;
109
+ const messageEl = formEl.querySelector(".jamwidgets-subscribe-message") as HTMLElement | null;
110
+ const submitBtn = formEl.querySelector('button[type="submit"]') as HTMLButtonElement | null;
111
+ const inputEl = formEl.querySelector('input[name="email"]') as HTMLInputElement | null;
112
+
113
+ if (!endpoint || !siteKey) {
114
+ console.error("JamWidgets Subscribe: missing siteKey or endpoint");
115
+ return;
116
+ }
117
+
118
+ // Collect form data
119
+ const formData = new FormData(formEl);
120
+ const data: Record<string, unknown> = {};
121
+ formData.forEach((value, key) => {
122
+ data[key] = value;
123
+ });
124
+
125
+ // Dispatch loading event
126
+ formEl.dispatchEvent(new CustomEvent("jamwidgets:loading"));
127
+
128
+ // Disable submit button and show loading state
129
+ if (submitBtn) {
130
+ submitBtn.disabled = true;
131
+ submitBtn.dataset.originalText = submitBtn.textContent || "";
132
+ submitBtn.textContent = "...";
133
+ }
134
+
135
+ try {
136
+ const response = await fetch(`${endpoint}/subscribe`, {
137
+ method: "POST",
138
+ headers: {
139
+ "Content-Type": "application/json",
140
+ "X-JamWidgets-Key": siteKey,
141
+ "X-JamWidgets-Visitor": getVisitorId(),
142
+ },
143
+ body: JSON.stringify(data),
144
+ });
145
+
146
+ if (!response.ok) {
147
+ const errorText = await response.text();
148
+ throw new Error(errorText || "Subscription failed");
149
+ }
150
+
151
+ const result: SubscribeResponse = await response.json();
152
+
153
+ // Show success message
154
+ if (messageEl) {
155
+ messageEl.textContent = customSuccessMessage || result.message;
156
+ messageEl.style.display = "block";
157
+ messageEl.className = "jamwidgets-subscribe-message jamwidgets-subscribe-success";
158
+ }
159
+
160
+ // Dispatch success event with response data
161
+ formEl.dispatchEvent(
162
+ new CustomEvent("jamwidgets:success", {
163
+ detail: result,
164
+ }),
165
+ );
166
+
167
+ // Clear input on success
168
+ if (inputEl) {
169
+ inputEl.value = "";
170
+ }
171
+ } catch (error) {
172
+ // Show error message
173
+ if (messageEl) {
174
+ messageEl.textContent = error instanceof Error ? error.message : "Something went wrong";
175
+ messageEl.style.display = "block";
176
+ messageEl.className = "jamwidgets-subscribe-message jamwidgets-subscribe-error";
177
+ }
178
+
179
+ formEl.dispatchEvent(
180
+ new CustomEvent("jamwidgets:error", {
181
+ detail: error,
182
+ }),
183
+ );
184
+ } finally {
185
+ // Re-enable submit button
186
+ if (submitBtn) {
187
+ submitBtn.disabled = false;
188
+ submitBtn.textContent = submitBtn.dataset.originalText || "Subscribe";
189
+ }
190
+ }
191
+ });
192
+ });
193
+ </script>
194
+
195
+ <style is:global>
196
+ @layer jamwidgets {
197
+ .jamwidgets-subscribe {
198
+ --jamwidgets-border-color: #d1d5db;
199
+ --jamwidgets-input-bg: #ffffff;
200
+ --jamwidgets-input-text: #111827;
201
+ --jamwidgets-button-bg: #4f46e5;
202
+ --jamwidgets-button-text: #ffffff;
203
+ --jamwidgets-button-hover: #4338ca;
204
+ --jamwidgets-success-bg: #dcfce7;
205
+ --jamwidgets-success-text: #166534;
206
+ --jamwidgets-success-border: #bbf7d0;
207
+ --jamwidgets-error-bg: #fef2f2;
208
+ --jamwidgets-error-text: #991b1b;
209
+ --jamwidgets-error-border: #fecaca;
210
+ --jamwidgets-focus-ring: rgba(79, 70, 229, 0.5);
211
+
212
+ position: relative;
213
+ }
214
+
215
+ /* Dark theme */
216
+ .jamwidgets-subscribe.jamwidgets-theme-dark {
217
+ --jamwidgets-border-color: #4b5563;
218
+ --jamwidgets-input-bg: #1f2937;
219
+ --jamwidgets-input-text: #f9fafb;
220
+ --jamwidgets-button-bg: #6366f1;
221
+ --jamwidgets-button-hover: #818cf8;
222
+ --jamwidgets-success-bg: rgba(34, 197, 94, 0.15);
223
+ --jamwidgets-success-text: #86efac;
224
+ --jamwidgets-success-border: rgba(34, 197, 94, 0.3);
225
+ --jamwidgets-error-bg: rgba(239, 68, 68, 0.15);
226
+ --jamwidgets-error-text: #fca5a5;
227
+ --jamwidgets-error-border: rgba(239, 68, 68, 0.3);
228
+ }
229
+
230
+ /* Auto theme - follows prefers-color-scheme */
231
+ @media (prefers-color-scheme: dark) {
232
+ .jamwidgets-subscribe.jamwidgets-theme-auto {
233
+ --jamwidgets-border-color: #4b5563;
234
+ --jamwidgets-input-bg: #1f2937;
235
+ --jamwidgets-input-text: #f9fafb;
236
+ --jamwidgets-button-bg: #6366f1;
237
+ --jamwidgets-button-hover: #818cf8;
238
+ --jamwidgets-success-bg: rgba(34, 197, 94, 0.15);
239
+ --jamwidgets-success-text: #86efac;
240
+ --jamwidgets-success-border: rgba(34, 197, 94, 0.3);
241
+ --jamwidgets-error-bg: rgba(239, 68, 68, 0.15);
242
+ --jamwidgets-error-text: #fca5a5;
243
+ --jamwidgets-error-border: rgba(239, 68, 68, 0.3);
244
+ }
245
+ }
246
+
247
+ .jamwidgets-subscribe-input-group {
248
+ display: flex;
249
+ gap: 0.5rem;
250
+ }
251
+
252
+ .jamwidgets-subscribe-input {
253
+ flex: 1;
254
+ padding: 0.625rem 0.875rem;
255
+ border: 1px solid var(--jamwidgets-border-color);
256
+ border-radius: 0.375rem;
257
+ background-color: var(--jamwidgets-input-bg);
258
+ color: var(--jamwidgets-input-text);
259
+ font-size: 0.875rem;
260
+ line-height: 1.25rem;
261
+ }
262
+
263
+ .jamwidgets-subscribe-input:focus {
264
+ outline: none;
265
+ border-color: var(--jamwidgets-button-bg);
266
+ box-shadow: 0 0 0 3px var(--jamwidgets-focus-ring);
267
+ }
268
+
269
+ .jamwidgets-subscribe-input::placeholder {
270
+ color: #9ca3af;
271
+ }
272
+
273
+ .jamwidgets-subscribe-button {
274
+ padding: 0.625rem 1rem;
275
+ border: none;
276
+ border-radius: 0.375rem;
277
+ background-color: var(--jamwidgets-button-bg);
278
+ color: var(--jamwidgets-button-text);
279
+ font-size: 0.875rem;
280
+ font-weight: 500;
281
+ cursor: pointer;
282
+ transition: background-color 0.15s ease;
283
+ white-space: nowrap;
284
+ }
285
+
286
+ .jamwidgets-subscribe-button:hover:not(:disabled) {
287
+ background-color: var(--jamwidgets-button-hover);
288
+ }
289
+
290
+ .jamwidgets-subscribe-button:disabled {
291
+ opacity: 0.6;
292
+ cursor: not-allowed;
293
+ }
294
+
295
+ .jamwidgets-subscribe-message {
296
+ margin-top: 0.75rem;
297
+ padding: 0.625rem 0.875rem;
298
+ border-radius: 0.375rem;
299
+ font-size: 0.875rem;
300
+ }
301
+
302
+ .jamwidgets-subscribe-success {
303
+ background-color: var(--jamwidgets-success-bg);
304
+ color: var(--jamwidgets-success-text);
305
+ border: 1px solid var(--jamwidgets-success-border);
306
+ }
307
+
308
+ .jamwidgets-subscribe-error {
309
+ background-color: var(--jamwidgets-error-bg);
310
+ color: var(--jamwidgets-error-text);
311
+ border: 1px solid var(--jamwidgets-error-border);
312
+ }
313
+ }
314
+ </style>