@herdingbits/trailhead-webawesome 0.1.2 → 0.3.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.
package/dist/adapter.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Web Awesome Design System Adapter
3
3
  */
4
- import type { DesignSystemAdapter, FeedbackAdapter } from '@herdingbits/trailhead-types/adapters';
4
+ import type { DesignSystemAdapter, FeedbackAdapter, AuthAdapter } from '@herdingbits/trailhead-types/adapters';
5
5
  export interface WebAwesomeAdapterConfig {
6
6
  /** URL where Web Awesome is hosted (CDN or local path). Defaults to `${shellUrl}/webawesome`. */
7
7
  webAwesomeUrl?: string;
@@ -10,6 +10,7 @@ export declare class WebAwesomeAdapter implements DesignSystemAdapter {
10
10
  name: string;
11
11
  version: string;
12
12
  feedback: FeedbackAdapter;
13
+ auth: AuthAdapter;
13
14
  private readonly config?;
14
15
  constructor(config?: WebAwesomeAdapterConfig);
15
16
  init(shellUrl: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAA4C,MAAM,uCAAuC,CAAC;AA2F5I,MAAM,WAAW,uBAAuB;IACtC,iGAAiG;IACjG,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,IAAI,SAAgB;IACpB,OAAO,SAAW;IAClB,QAAQ,EAAE,eAAe,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAA0B;gBAEtC,MAAM,CAAC,EAAE,uBAAuB;IAKtC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAkB5C"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAA4C,WAAW,EAAuC,MAAM,uCAAuC,CAAC;AAsM9L,MAAM,WAAW,uBAAuB;IACtC,iGAAiG;IACjG,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,IAAI,SAAgB;IACpB,OAAO,SAAW;IAClB,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAA0B;gBAEtC,MAAM,CAAC,EAAE,uBAAuB;IAMtC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAkB5C"}
package/dist/adapter.js CHANGED
@@ -1,29 +1,64 @@
1
+ const TOAST_ICONS = {
2
+ success: "circle-check",
3
+ error: "circle-exclamation",
4
+ warning: "triangle-exclamation",
5
+ info: "circle-info",
6
+ };
7
+ // wa-callout has no "error"/"info" variant — map onto its brand/danger vocabulary.
8
+ function toastCalloutVariant(variant) {
9
+ if (variant === "error")
10
+ return "danger";
11
+ if (variant === "info")
12
+ return "brand";
13
+ return variant;
14
+ }
15
+ // DialogButton.variant is a free-form string set by callers (only "primary"/"secondary" are
16
+ // used anywhere in practice); map it onto wa-button's variant/appearance vocabulary.
17
+ function dialogButtonAppearance(variant) {
18
+ if (variant === "primary")
19
+ return { variant: "brand", appearance: "filled" };
20
+ if (variant === "secondary")
21
+ return { variant: "neutral", appearance: "outlined" };
22
+ return { variant: "neutral", appearance: "plain" };
23
+ }
1
24
  class WebAwesomeFeedbackAdapter {
2
25
  constructor() {
3
- this.busyOverlay = null;
26
+ this.busyDialog = null;
27
+ this.busyActive = false;
4
28
  this.toastContainer = null;
5
29
  }
6
30
  showBusy(message) {
7
- if (!this.busyOverlay) {
8
- this.busyOverlay = document.createElement("div");
9
- this.busyOverlay.id = "shell-busy-overlay";
10
- this.busyOverlay.innerHTML = `
31
+ if (!this.busyDialog) {
32
+ const dialog = document.createElement("wa-dialog");
33
+ dialog.setAttribute("without-header", "");
34
+ dialog.className = "shell-busy-dialog";
35
+ dialog.innerHTML = `
11
36
  <div class="shell-busy-content">
12
- <div class="shell-spinner"></div>
37
+ <wa-spinner></wa-spinner>
13
38
  <div class="shell-busy-message"></div>
14
39
  </div>
15
40
  `;
16
- document.body.appendChild(this.busyOverlay);
41
+ // Busy is a blocking state — Escape (or any other close trigger) must not dismiss it
42
+ // early; only our own clearBusy() call is allowed to close it. See "Preventing the
43
+ // Dialog from Closing" in the wa-dialog docs.
44
+ dialog.addEventListener("wa-hide", (e) => {
45
+ if (this.busyActive)
46
+ e.preventDefault();
47
+ });
48
+ document.body.appendChild(dialog);
49
+ this.busyDialog = dialog;
17
50
  }
18
- const messageEl = this.busyOverlay.querySelector(".shell-busy-message");
51
+ const messageEl = this.busyDialog.querySelector(".shell-busy-message");
19
52
  if (messageEl) {
20
53
  messageEl.textContent = message;
21
54
  }
22
- this.busyOverlay.style.display = "flex";
55
+ this.busyActive = true;
56
+ this.busyDialog.open = true;
23
57
  }
24
58
  clearBusy() {
25
- if (this.busyOverlay) {
26
- this.busyOverlay.style.display = "none";
59
+ this.busyActive = false;
60
+ if (this.busyDialog) {
61
+ this.busyDialog.open = false;
27
62
  }
28
63
  }
29
64
  showToast(message, variant, duration = 3000) {
@@ -32,9 +67,13 @@ class WebAwesomeFeedbackAdapter {
32
67
  this.toastContainer.id = "shell-toast-container";
33
68
  document.body.appendChild(this.toastContainer);
34
69
  }
35
- const toast = document.createElement("div");
36
- toast.className = `shell-toast shell-toast-${variant}`;
37
- toast.textContent = message;
70
+ const toast = document.createElement("wa-callout");
71
+ toast.setAttribute("variant", toastCalloutVariant(variant));
72
+ toast.setAttribute("appearance", "filled");
73
+ toast.className = "shell-toast";
74
+ // "solid" (the default) is the only style free Font Awesome kits are guaranteed to carry —
75
+ // "regular"/"light"/"thin" are Pro-only and 403 silently on a free kit.
76
+ toast.innerHTML = `<wa-icon slot="icon" name="${TOAST_ICONS[variant]}"></wa-icon>${message}`;
38
77
  this.toastContainer.appendChild(toast);
39
78
  setTimeout(() => toast.classList.add("shell-toast-show"), 10);
40
79
  setTimeout(() => {
@@ -44,35 +83,100 @@ class WebAwesomeFeedbackAdapter {
44
83
  }
45
84
  showDialog(config) {
46
85
  return new Promise((resolve) => {
47
- const dialog = document.createElement("div");
48
- dialog.className = "shell-dialog-overlay";
86
+ let settled = false;
87
+ const dialog = document.createElement("wa-dialog");
88
+ if (config.title) {
89
+ dialog.setAttribute("label", config.title);
90
+ }
91
+ else {
92
+ dialog.setAttribute("without-header", "");
93
+ }
94
+ dialog.setAttribute("light-dismiss", "");
95
+ dialog.className = "shell-dialog";
49
96
  dialog.innerHTML = `
50
- <div class="shell-dialog">
51
- ${config.title ? `<div class="shell-dialog-title">${config.title}</div>` : ''}
52
- <div class="shell-dialog-message">${config.message}</div>
53
- <div class="shell-dialog-buttons">
54
- ${config.buttons
55
- .map((btn) => `<button class="shell-btn shell-btn-${btn.variant || "default"}" data-value="${btn.value}">${btn.label}</button>`)
97
+ <p class="shell-dialog-message">${config.message}</p>
98
+ ${config.buttons
99
+ .map((btn) => {
100
+ const { variant, appearance } = dialogButtonAppearance(btn.variant);
101
+ return `<wa-button slot="footer" variant="${variant}" appearance="${appearance}" data-value="${btn.value}">${btn.label}</wa-button>`;
102
+ })
56
103
  .join("")}
57
- </div>
58
- </div>
59
104
  `;
60
- dialog.querySelectorAll("button").forEach((btn) => {
61
- btn.onclick = () => {
62
- const value = btn.getAttribute("data-value");
63
- dialog.remove();
64
- resolve({ value });
65
- };
66
- });
67
- // Click outside to cancel
68
- dialog.onclick = (e) => {
69
- if (e.target === dialog) {
70
- dialog.remove();
71
- resolve({ value: null });
72
- }
105
+ const settleOnce = (value) => {
106
+ if (settled)
107
+ return;
108
+ settled = true;
109
+ resolve({ value });
110
+ dialog.open = false;
73
111
  };
112
+ dialog.addEventListener("wa-after-hide", () => dialog.remove());
113
+ // Escape, the header close button, and light-dismiss all fire wa-hide — treat as no selection.
114
+ dialog.addEventListener("wa-hide", () => settleOnce(null));
115
+ dialog.querySelectorAll("wa-button[data-value]").forEach((btn) => {
116
+ btn.addEventListener("click", () => settleOnce(btn.getAttribute("data-value")));
117
+ });
74
118
  document.body.appendChild(dialog);
119
+ dialog.open = true;
120
+ });
121
+ }
122
+ }
123
+ class WebAwesomeAuthAdapter {
124
+ promptCredentials(errorMessage) {
125
+ let settle;
126
+ const result = new Promise((resolve) => {
127
+ settle = resolve;
128
+ });
129
+ let settled = false;
130
+ const formId = `shell-auth-form-${Math.random().toString(36).slice(2)}`;
131
+ const dialog = document.createElement("wa-dialog");
132
+ dialog.setAttribute("label", "Session Expired");
133
+ dialog.setAttribute("light-dismiss", "");
134
+ dialog.className = "shell-auth-dialog";
135
+ dialog.innerHTML = `
136
+ <p class="shell-auth-message">Sign in to continue where you left off.</p>
137
+ ${errorMessage
138
+ ? `<wa-callout variant="danger" size="small" class="shell-auth-error">
139
+ <wa-icon slot="icon" name="circle-exclamation"></wa-icon>
140
+ ${errorMessage}
141
+ </wa-callout>`
142
+ : ""}
143
+ <form id="${formId}" class="shell-auth-form">
144
+ <wa-input name="username" label="Username" autocomplete="username" autofocus required></wa-input>
145
+ <wa-input name="password" type="password" label="Password" autocomplete="current-password" password-toggle required></wa-input>
146
+ </form>
147
+ <wa-button slot="footer" appearance="outlined" data-action="cancel">Cancel</wa-button>
148
+ <wa-button slot="footer" variant="brand" appearance="filled" type="submit" form="${formId}">Sign In</wa-button>
149
+ `;
150
+ const settleOnce = (value) => {
151
+ if (settled)
152
+ return;
153
+ settled = true;
154
+ settle(value);
155
+ dialog.open = false;
156
+ };
157
+ // wa-dialog owns its own close animation; only remove the element once it's finished.
158
+ dialog.addEventListener("wa-after-hide", () => dialog.remove());
159
+ // Escape, the header close button, and light-dismiss all fire wa-hide — treat every one as cancel.
160
+ dialog.addEventListener("wa-hide", () => settleOnce(null));
161
+ const form = dialog.querySelector("form");
162
+ form.addEventListener("submit", (e) => {
163
+ e.preventDefault();
164
+ const data = new FormData(form);
165
+ settleOnce({
166
+ username: String(data.get("username") ?? ""),
167
+ password: String(data.get("password") ?? ""),
168
+ });
169
+ });
170
+ dialog.querySelector('[data-action="cancel"]')?.addEventListener("click", () => settleOnce(null));
171
+ document.body.appendChild(dialog);
172
+ dialog.open = true;
173
+ requestAnimationFrame(() => {
174
+ dialog.querySelector('wa-input[name="username"]')?.focus();
75
175
  });
176
+ return {
177
+ result,
178
+ close: () => settleOnce(null),
179
+ };
76
180
  }
77
181
  }
78
182
  export class WebAwesomeAdapter {
@@ -81,6 +185,7 @@ export class WebAwesomeAdapter {
81
185
  this.version = "1.0.0";
82
186
  this.config = config;
83
187
  this.feedback = new WebAwesomeFeedbackAdapter();
188
+ this.auth = new WebAwesomeAuthAdapter();
84
189
  }
85
190
  async init(shellUrl) {
86
191
  try {
package/dist/shell.css CHANGED
@@ -123,39 +123,13 @@ body {
123
123
  }
124
124
 
125
125
  /* Busy overlay */
126
- #shell-busy-overlay {
127
- position: fixed;
128
- top: 0;
129
- left: 0;
130
- right: 0;
131
- bottom: 0;
132
- background-color: rgba(0, 0, 0, 0.5);
133
- display: none;
134
- align-items: center;
135
- justify-content: center;
136
- z-index: 9999;
137
- }
138
-
139
126
  .shell-busy-content {
140
- background: white;
141
- padding: 2rem;
142
- border-radius: 8px;
127
+ display: flex;
128
+ flex-direction: column;
129
+ align-items: center;
130
+ gap: 1rem;
131
+ padding: 1rem 2rem 1.5rem;
143
132
  text-align: center;
144
- min-width: 200px;
145
- }
146
-
147
- .shell-spinner {
148
- width: 40px;
149
- height: 40px;
150
- border: 4px solid #e5e7eb;
151
- border-top-color: #3b82f6;
152
- border-radius: 50%;
153
- animation: shell-spin 0.8s linear infinite;
154
- margin: 0 auto 1rem;
155
- }
156
-
157
- @keyframes shell-spin {
158
- to { transform: rotate(360deg); }
159
133
  }
160
134
 
161
135
  .shell-busy-message {
@@ -175,15 +149,11 @@ body {
175
149
  }
176
150
 
177
151
  .shell-toast {
178
- padding: 1rem 1.5rem;
179
- border-radius: 6px;
180
- color: white;
181
- font-size: 0.875rem;
152
+ max-width: 400px;
182
153
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
183
154
  opacity: 0;
184
155
  transform: translateX(100%);
185
156
  transition: all 0.3s;
186
- max-width: 400px;
187
157
  }
188
158
 
189
159
  .shell-toast-show {
@@ -191,97 +161,33 @@ body {
191
161
  transform: translateX(0);
192
162
  }
193
163
 
194
- .shell-toast-success {
195
- background-color: #10b981;
196
- }
197
-
198
- .shell-toast-error {
199
- background-color: #ef4444;
200
- }
201
-
202
- .shell-toast-warning {
203
- background-color: #f59e0b;
204
- }
205
-
206
- .shell-toast-info {
207
- background-color: #3b82f6;
208
- }
209
-
210
164
  /* Dialog */
211
- .shell-dialog-overlay {
212
- position: fixed;
213
- top: 0;
214
- left: 0;
215
- right: 0;
216
- bottom: 0;
217
- background-color: rgba(0, 0, 0, 0.5);
218
- display: flex;
219
- align-items: center;
220
- justify-content: center;
221
- z-index: 10001;
222
- }
223
-
224
165
  .shell-dialog {
225
- background: white;
226
- border-radius: 8px;
227
- padding: 1.5rem;
228
- min-width: 400px;
229
- max-width: 600px;
230
- box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15);
231
- }
232
-
233
- .shell-dialog-title {
234
- font-size: 1.25rem;
235
- font-weight: 600;
236
- color: #1e293b;
237
- margin-bottom: 1rem;
166
+ --width: 440px;
238
167
  }
239
168
 
240
169
  .shell-dialog-message {
241
170
  color: #64748b;
242
- margin-bottom: 1.5rem;
243
171
  line-height: 1.5;
244
172
  }
245
173
 
246
- .shell-dialog-buttons {
247
- display: flex;
248
- gap: 0.75rem;
249
- justify-content: flex-end;
174
+ /* Re-authentication prompt */
175
+ .shell-auth-dialog {
176
+ --width: 400px;
250
177
  }
251
178
 
252
- .shell-btn {
253
- padding: 0.5rem 1rem;
254
- border: none;
255
- border-radius: 6px;
256
- font-size: 0.875rem;
257
- font-weight: 500;
258
- cursor: pointer;
259
- transition: all 0.2s;
260
- }
261
-
262
- .shell-btn-primary {
263
- background-color: #3b82f6;
264
- color: white;
265
- }
266
-
267
- .shell-btn-primary:hover {
268
- background-color: #2563eb;
269
- }
270
-
271
- .shell-btn-secondary {
272
- background-color: #e5e7eb;
273
- color: #1e293b;
274
- }
275
-
276
- .shell-btn-secondary:hover {
277
- background-color: #d1d5db;
179
+ .shell-auth-message {
180
+ color: #64748b;
181
+ margin-bottom: 1rem;
182
+ line-height: 1.5;
278
183
  }
279
184
 
280
- .shell-btn-default {
281
- background-color: #f3f4f6;
282
- color: #1e293b;
185
+ .shell-auth-error {
186
+ margin-bottom: 1rem;
283
187
  }
284
188
 
285
- .shell-btn-default:hover {
286
- background-color: #e5e7eb;
189
+ .shell-auth-form {
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 0.75rem;
287
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herdingbits/trailhead-webawesome",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Web Awesome web components adapter for Trailhead. Vanilla TypeScript, no React required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,12 +21,12 @@
21
21
  "build": "npm run clean && tsc && cp src/shell.css dist/"
22
22
  },
23
23
  "peerDependencies": {
24
- "@herdingbits/trailhead-core": "^0.1.0",
25
- "@awesome.me/webawesome": "^3.6.0"
24
+ "@awesome.me/webawesome": "^3.6.0",
25
+ "@herdingbits/trailhead-core": "^0.3.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@herdingbits/trailhead-types": "^0.1.0",
29
28
  "@awesome.me/webawesome": "^3.10.0",
29
+ "@herdingbits/trailhead-types": "^0.3.0",
30
30
  "typescript": "^6.0.3"
31
31
  },
32
32
  "keywords": [