@jamwidgets/astro 0.6.3 → 0.6.6

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.
package/README.md CHANGED
@@ -234,6 +234,7 @@ import Subscribe from "@jamwidgets/astro/Subscribe";
234
234
  - `endpoint` - Base URL (default: `https://jamwidgets.com`)
235
235
  - `buttonText` - Submit button text (default: `'Subscribe'`)
236
236
  - `placeholder` - Email input placeholder
237
+ - `label` - Accessible name for the email input (default: `'Email address'`)
237
238
  - `successMessage` - Custom success message
238
239
  - `theme` - `'light'` | `'dark'` | `'auto'` (default: `'light'`)
239
240
  - `class` - Additional CSS class
@@ -317,6 +318,10 @@ Set semantic CSS custom properties on a wrapper or the widget itself to match yo
317
318
 
318
319
  Status colors can be customized with `--jamwidgets-color-success`, `--jamwidgets-color-error`, and `--jamwidgets-color-notice`. Components adapt to their container width, so subscribe, waitlist, comments, and feedback controls stack when embedded in a narrow column.
319
320
 
321
+ ### Cascade layers and Tailwind
322
+
323
+ Widget styles live in a `jamwidgets` cascade layer. Every component declares the order `@layer theme, base, components, utilities, jamwidgets;` before its rules, so the widgets rank above Tailwind v4's layers (including Preflight in `base`) whichever stylesheet the browser sees first. Your unlayered CSS still overrides the widgets. If your site declares its own layer order, keep `jamwidgets` after `base` or Preflight will reset the widgets' padding, borders and buttons.
324
+
320
325
  ## License
321
326
 
322
327
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamwidgets/astro",
3
- "version": "0.6.3",
3
+ "version": "0.6.6",
4
4
  "description": "Astro components and content loaders for Jamwidgets",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,7 +60,7 @@
60
60
  ],
61
61
  "license": "MIT",
62
62
  "dependencies": {
63
- "@jamwidgets/core": "^0.5.2"
63
+ "@jamwidgets/core": "^0.6.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "astro": "^5.9.0 || ^6.0.0 || ^7.0.0"
@@ -54,16 +54,7 @@ import Theme from "./Theme.astro";
54
54
  isDismissible: boolean;
55
55
  }
56
56
 
57
- // Get or create a visitor ID for anonymous users
58
- const VISITOR_KEY = "jamwidgets_visitor_id";
59
- function getVisitorId(): string {
60
- let id = localStorage.getItem(VISITOR_KEY);
61
- if (!id) {
62
- id = crypto.randomUUID();
63
- localStorage.setItem(VISITOR_KEY, id);
64
- }
65
- return id;
66
- }
57
+ import { getVisitorId } from "@jamwidgets/core";
67
58
 
68
59
  document.querySelectorAll("[data-jamwidgets-announcements]").forEach((container) => {
69
60
  const endpoint = (container as HTMLElement).dataset.endpoint;
@@ -160,6 +151,7 @@ import Theme from "./Theme.astro";
160
151
  </script>
161
152
 
162
153
  <style is:global>
154
+ @layer theme, base, components, utilities, jamwidgets;
163
155
  @layer jamwidgets {
164
156
  .jamwidgets-announcements {
165
157
  display: flex;
@@ -262,6 +262,7 @@ import Theme from "./Theme.astro";
262
262
  </script>
263
263
 
264
264
  <style is:global>
265
+ @layer theme, base, components, utilities, jamwidgets;
265
266
  @layer jamwidgets {
266
267
  .jamwidgets-comments {
267
268
  color: var(--jamwidgets-text-color);
package/src/Embed.astro CHANGED
@@ -64,16 +64,7 @@ import Theme from "./Theme.astro";
64
64
  userVotes?: string[];
65
65
  }
66
66
 
67
- // Get or create a visitor ID for anonymous users
68
- const VISITOR_KEY = "jamwidgets_visitor_id";
69
- function getVisitorId(): string {
70
- let id = localStorage.getItem(VISITOR_KEY);
71
- if (!id) {
72
- id = crypto.randomUUID();
73
- localStorage.setItem(VISITOR_KEY, id);
74
- }
75
- return id;
76
- }
67
+ import { apiError, getVisitorId } from "@jamwidgets/core";
77
68
 
78
69
  class JamwidgetsEmbed extends HTMLElement {
79
70
  connectedCallback() {
@@ -280,8 +271,7 @@ import Theme from "./Theme.astro";
280
271
  });
281
272
 
282
273
  if (!response.ok) {
283
- const error = await response.json();
284
- throw new Error(error.message || "Failed to vote");
274
+ throw await apiError(response, "Failed to vote");
285
275
  }
286
276
 
287
277
  const data = await response.json();
@@ -321,6 +311,7 @@ import Theme from "./Theme.astro";
321
311
  </script>
322
312
 
323
313
  <style is:global>
314
+ @layer theme, base, components, utilities, jamwidgets;
324
315
  @layer jamwidgets {
325
316
  .jamwidgets-poll {
326
317
  font-family: inherit;
@@ -3,7 +3,7 @@
3
3
  * Jamwidgets Feedback Component
4
4
  *
5
5
  * Displays a feedback form for collecting user suggestions.
6
- * Supports different feedback types (bug, feature, improvement, other).
6
+ * Supports the API's feedback types: feature, bug and general.
7
7
  *
8
8
  * @example
9
9
  * <Feedback siteKey={import.meta.env.JAMWIDGETS_SITE_KEY} />
@@ -62,11 +62,7 @@ import Theme from "./Theme.astro";
62
62
  <span>Bug</span>
63
63
  </label>
64
64
  <label class="jamwidgets-feedback-type">
65
- <input type="radio" name="feedbackType" value="improvement" />
66
- <span>Improvement</span>
67
- </label>
68
- <label class="jamwidgets-feedback-type">
69
- <input type="radio" name="feedbackType" value="other" />
65
+ <input type="radio" name="feedbackType" value="general" />
70
66
  <span>Other</span>
71
67
  </label>
72
68
  </div>
@@ -90,16 +86,7 @@ import Theme from "./Theme.astro";
90
86
  </div>
91
87
 
92
88
  <script>
93
- // Get or create a visitor ID for anonymous users
94
- const VISITOR_KEY = "jamwidgets_visitor_id";
95
- function getVisitorId(): string {
96
- let id = localStorage.getItem(VISITOR_KEY);
97
- if (!id) {
98
- id = crypto.randomUUID();
99
- localStorage.setItem(VISITOR_KEY, id);
100
- }
101
- return id;
102
- }
89
+ import { apiError, getVisitorId } from "@jamwidgets/core";
103
90
 
104
91
  document.querySelectorAll("[data-jamwidgets-feedback]").forEach((container) => {
105
92
  const endpoint = (container as HTMLElement).dataset.endpoint;
@@ -126,7 +113,7 @@ import Theme from "./Theme.astro";
126
113
  if (!content) return;
127
114
 
128
115
  const typeInput = form.querySelector('input[name="feedbackType"]:checked') as HTMLInputElement | null;
129
- const feedbackType = typeInput?.value || "other";
116
+ const feedbackType = typeInput?.value || "general";
130
117
 
131
118
  submitBtn.disabled = true;
132
119
  submitBtn.textContent = "Sending...";
@@ -141,8 +128,7 @@ import Theme from "./Theme.astro";
141
128
  });
142
129
 
143
130
  if (!response.ok) {
144
- const error = await response.json();
145
- throw new Error(error.message || "Failed to submit feedback");
131
+ throw await apiError(response, "Failed to submit feedback");
146
132
  }
147
133
 
148
134
  textarea.value = "";
@@ -172,6 +158,7 @@ import Theme from "./Theme.astro";
172
158
  </script>
173
159
 
174
160
  <style is:global>
161
+ @layer theme, base, components, utilities, jamwidgets;
175
162
  @layer jamwidgets {
176
163
  .jamwidgets-feedback {
177
164
  font-family: inherit;
package/src/Form.astro CHANGED
@@ -67,7 +67,7 @@ import Theme from "./Theme.astro";
67
67
  </form>
68
68
 
69
69
  <script>
70
- import { generateUUID, getAttributionHeaders, getVisitorId } from "@jamwidgets/core";
70
+ import { apiError, generateUUID, getAttributionHeaders, getVisitorId } from "@jamwidgets/core";
71
71
 
72
72
  interface FormSubmitResponse {
73
73
  success: boolean;
@@ -145,8 +145,7 @@ import Theme from "./Theme.astro";
145
145
  });
146
146
 
147
147
  if (!response.ok) {
148
- const errorText = await response.text();
149
- throw new Error(errorText || "Submission failed");
148
+ throw await apiError(response, "Submission failed");
150
149
  }
151
150
 
152
151
  const result: FormSubmitResponse = await response.json();
@@ -193,6 +192,7 @@ import Theme from "./Theme.astro";
193
192
  </script>
194
193
 
195
194
  <style is:global>
195
+ @layer theme, base, components, utilities, jamwidgets;
196
196
  @layer jamwidgets {
197
197
  .jamwidgets-form-message {
198
198
  margin-top: 1rem;
package/src/Poll.astro CHANGED
@@ -89,16 +89,7 @@ import Theme from "./Theme.astro";
89
89
  userVotes?: string[];
90
90
  }
91
91
 
92
- // Get or create a visitor ID for anonymous users
93
- const VISITOR_KEY = "jamwidgets_visitor_id";
94
- function getVisitorId(): string {
95
- let id = localStorage.getItem(VISITOR_KEY);
96
- if (!id) {
97
- id = crypto.randomUUID();
98
- localStorage.setItem(VISITOR_KEY, id);
99
- }
100
- return id;
101
- }
92
+ import { apiError, getVisitorId } from "@jamwidgets/core";
102
93
 
103
94
  document.querySelectorAll("[data-jamwidgets-poll]").forEach((container) => {
104
95
  const endpoint = (container as HTMLElement).dataset.endpoint;
@@ -264,8 +255,7 @@ import Theme from "./Theme.astro";
264
255
  });
265
256
 
266
257
  if (!response.ok) {
267
- const error = await response.json();
268
- throw new Error(error.message || "Failed to vote");
258
+ throw await apiError(response, "Failed to vote");
269
259
  }
270
260
 
271
261
  const data = await response.json();
@@ -302,6 +292,7 @@ import Theme from "./Theme.astro";
302
292
  </script>
303
293
 
304
294
  <style is:global>
295
+ @layer theme, base, components, utilities, jamwidgets;
305
296
  @layer jamwidgets {
306
297
  .jamwidgets-poll {
307
298
  font-family: inherit;
@@ -106,11 +106,14 @@ import Theme from "./Theme.astro";
106
106
  "X-Jamwidgets-Visitor": visitorId,
107
107
  };
108
108
 
109
- // Track which reactions the user has made (stored in localStorage)
110
109
  const storageKey = `jamwidgets-reactions-${pageId}`;
111
- const userReactions = new Set<string>(
112
- JSON.parse(localStorage.getItem(storageKey) || "[]")
113
- );
110
+ const userReactions = new Set<string>();
111
+ try {
112
+ const stored = JSON.parse(localStorage.getItem(storageKey) || "[]");
113
+ if (Array.isArray(stored)) {
114
+ for (const type of stored) if (typeof type === "string") userReactions.add(type);
115
+ }
116
+ } catch {}
114
117
 
115
118
  // Apply initial "reacted" state
116
119
  container.querySelectorAll(".jamwidgets-reaction-btn").forEach((btn) => {
@@ -134,7 +137,7 @@ import Theme from "./Theme.astro";
134
137
 
135
138
  // Sync with server's user reactions
136
139
  serverUserReactions.forEach((type: string) => userReactions.add(type));
137
- localStorage.setItem(storageKey, JSON.stringify([...userReactions]));
140
+ try { localStorage.setItem(storageKey, JSON.stringify([...userReactions])); } catch {}
138
141
 
139
142
  container.querySelectorAll(".jamwidgets-reaction-btn").forEach((btn) => {
140
143
  const type = (btn as HTMLElement).dataset.reactionType;
@@ -204,7 +207,7 @@ import Theme from "./Theme.astro";
204
207
  }),
205
208
  );
206
209
  }
207
- localStorage.setItem(storageKey, JSON.stringify([...userReactions]));
210
+ try { localStorage.setItem(storageKey, JSON.stringify([...userReactions])); } catch {}
208
211
  } catch (e) {
209
212
  console.error(isReacted ? "Failed to remove reaction:" : "Failed to add reaction:", e);
210
213
  } finally {
@@ -218,6 +221,7 @@ import Theme from "./Theme.astro";
218
221
  </script>
219
222
 
220
223
  <style is:global>
224
+ @layer theme, base, components, utilities, jamwidgets;
221
225
  @layer jamwidgets {
222
226
  .jamwidgets-reactions {
223
227
  display: flex;
@@ -26,6 +26,8 @@ interface Props {
26
26
  buttonText?: string;
27
27
  /** Email input placeholder (default: 'your@email.com') */
28
28
  placeholder?: string;
29
+ /** Accessible name for the email input (default: 'Email address') */
30
+ label?: string;
29
31
  /** Success message override (default: uses server response) */
30
32
  successMessage?: string;
31
33
  /** Theme preset. Defaults to inheriting the host page. */
@@ -42,6 +44,7 @@ const {
42
44
  endpoint = DEFAULT_ENDPOINT,
43
45
  buttonText = "Subscribe",
44
46
  placeholder = "your@email.com",
47
+ label = "Email address",
45
48
  successMessage,
46
49
  theme = "inherit",
47
50
  class: className = "",
@@ -68,6 +71,7 @@ import Theme from "./Theme.astro";
68
71
  name="email"
69
72
  required
70
73
  placeholder={placeholder}
74
+ aria-label={label}
71
75
  class="jamwidgets-subscribe-input"
72
76
  autocomplete="email"
73
77
  />
@@ -93,16 +97,7 @@ import Theme from "./Theme.astro";
93
97
  message: string;
94
98
  }
95
99
 
96
- // Get or create a visitor ID for anonymous users
97
- const VISITOR_KEY = "jamwidgets_visitor_id";
98
- function getVisitorId(): string {
99
- let id = localStorage.getItem(VISITOR_KEY);
100
- if (!id) {
101
- id = crypto.randomUUID();
102
- localStorage.setItem(VISITOR_KEY, id);
103
- }
104
- return id;
105
- }
100
+ import { apiError, getVisitorId } from "@jamwidgets/core";
106
101
 
107
102
  document.querySelectorAll("[data-jamwidgets-subscribe]").forEach((form) => {
108
103
  form.addEventListener("submit", async (e) => {
@@ -150,8 +145,7 @@ import Theme from "./Theme.astro";
150
145
  });
151
146
 
152
147
  if (!response.ok) {
153
- const errorText = await response.text();
154
- throw new Error(errorText || "Subscription failed");
148
+ throw await apiError(response, "Subscription failed");
155
149
  }
156
150
 
157
151
  const result: SubscribeResponse = await response.json();
@@ -199,6 +193,7 @@ import Theme from "./Theme.astro";
199
193
  </script>
200
194
 
201
195
  <style is:global>
196
+ @layer theme, base, components, utilities, jamwidgets;
202
197
  @layer jamwidgets {
203
198
  .jamwidgets-subscribe {
204
199
  position: relative;
@@ -71,7 +71,7 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
71
71
  </form>
72
72
 
73
73
  <script>
74
- import { getAttributionHeaders, getVisitorId } from "@jamwidgets/core";
74
+ import { apiError, getAttributionHeaders, getVisitorId } from "@jamwidgets/core";
75
75
 
76
76
  interface SubscribeResponse {
77
77
  success: boolean;
@@ -114,8 +114,7 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
114
114
  });
115
115
 
116
116
  if (!response.ok) {
117
- const errorText = await response.text();
118
- throw new Error(errorText || "Subscription failed");
117
+ throw await apiError(response, "Subscription failed");
119
118
  }
120
119
 
121
120
  const result: SubscribeResponse = await response.json();
package/src/Theme.astro CHANGED
@@ -1,4 +1,11 @@
1
1
  <style is:global>
2
+ /* Whichever stylesheet names a layer first fixes its order. If these widget
3
+ styles load before a Tailwind site's CSS, `jamwidgets` would otherwise
4
+ become the lowest layer and Tailwind's base reset would strip widget
5
+ input borders. Naming Tailwind's layers first, and `jamwidgets` last,
6
+ gives the same order whichever CSS loads first. Every widget repeats
7
+ this statement; unlayered site CSS still wins. */
8
+ @layer theme, base, components, utilities, jamwidgets;
2
9
  @layer jamwidgets {
3
10
  :where(
4
11
  .jamwidgets-announcements,
package/src/Views.astro CHANGED
@@ -65,16 +65,7 @@ import Theme from "./Theme.astro";
65
65
  </div>
66
66
 
67
67
  <script>
68
- // Get or create a visitor ID for anonymous users
69
- const VISITOR_KEY = "jamwidgets_visitor_id";
70
- function getVisitorId(): string {
71
- let id = localStorage.getItem(VISITOR_KEY);
72
- if (!id) {
73
- id = crypto.randomUUID();
74
- localStorage.setItem(VISITOR_KEY, id);
75
- }
76
- return id;
77
- }
68
+ import { getVisitorId } from "@jamwidgets/core";
78
69
 
79
70
  document.querySelectorAll("[data-jamwidgets-views]").forEach((container) => {
80
71
  const endpoint = (container as HTMLElement).dataset.endpoint;
@@ -158,6 +149,7 @@ import Theme from "./Theme.astro";
158
149
  </script>
159
150
 
160
151
  <style is:global>
152
+ @layer theme, base, components, utilities, jamwidgets;
161
153
  @layer jamwidgets {
162
154
  .jamwidgets-views {
163
155
  display: inline-flex;
@@ -17,6 +17,8 @@ interface Props {
17
17
  theme?: "inherit" | "light" | "dark" | "auto";
18
18
  /** Placeholder text for the email input (default: 'Enter your email') */
19
19
  placeholder?: string;
20
+ /** Accessible name for the email input (default: 'Email address') */
21
+ label?: string;
20
22
  /** Button text (default: 'Join Waitlist') */
21
23
  buttonText?: string;
22
24
  /** Success message (default: "You're on the list!") */
@@ -33,6 +35,7 @@ const {
33
35
  endpoint = DEFAULT_ENDPOINT,
34
36
  theme = "inherit",
35
37
  placeholder = "Enter your email",
38
+ label = "Email address",
36
39
  buttonText = "Join Waitlist",
37
40
  successMessage = "You're on the list!",
38
41
  class: className = "",
@@ -59,6 +62,8 @@ import Theme from "./Theme.astro";
59
62
  name="email"
60
63
  class="jamwidgets-waitlist-input"
61
64
  placeholder={placeholder}
65
+ aria-label={label}
66
+ autocomplete="email"
62
67
  required
63
68
  />
64
69
  <button type="submit" class="jamwidgets-waitlist-submit">
@@ -71,16 +76,7 @@ import Theme from "./Theme.astro";
71
76
  </div>
72
77
 
73
78
  <script>
74
- // Get or create a visitor ID for anonymous users
75
- const VISITOR_KEY = "jamwidgets_visitor_id";
76
- function getVisitorId(): string {
77
- let id = localStorage.getItem(VISITOR_KEY);
78
- if (!id) {
79
- id = crypto.randomUUID();
80
- localStorage.setItem(VISITOR_KEY, id);
81
- }
82
- return id;
83
- }
79
+ import { apiError, getVisitorId } from "@jamwidgets/core";
84
80
 
85
81
  document.querySelectorAll("[data-jamwidgets-waitlist]").forEach((container) => {
86
82
  const endpoint = (container as HTMLElement).dataset.endpoint;
@@ -120,8 +116,7 @@ import Theme from "./Theme.astro";
120
116
  });
121
117
 
122
118
  if (!response.ok) {
123
- const error = await response.json();
124
- throw new Error(error.message || "Failed to join waitlist");
119
+ throw await apiError(response, "Failed to join waitlist");
125
120
  }
126
121
 
127
122
  // Hide form and show success
@@ -152,6 +147,7 @@ import Theme from "./Theme.astro";
152
147
  </script>
153
148
 
154
149
  <style is:global>
150
+ @layer theme, base, components, utilities, jamwidgets;
155
151
  @layer jamwidgets {
156
152
  .jamwidgets-waitlist {
157
153
  font-family: inherit;