@hexclave/next 1.0.74 → 1.0.76

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.
Files changed (111) hide show
  1. package/dist/{admin-app-impl-FZdANO62.d.ts → admin-app-impl-CsH-gVGz.d.ts} +2 -2
  2. package/dist/{admin-app-impl-FZdANO62.d.ts.map → admin-app-impl-CsH-gVGz.d.ts.map} +1 -1
  3. package/dist/components/api-key-dialogs.d.ts +1 -1
  4. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  5. package/dist/dev-tool/dev-tool-core.js +2 -2
  6. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  7. package/dist/dev-tool/index.d.ts.map +1 -1
  8. package/dist/dev-tool/index.js +2 -8
  9. package/dist/dev-tool/index.js.map +1 -1
  10. package/dist/esm/components/api-key-dialogs.d.ts +1 -1
  11. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  12. package/dist/esm/dev-tool/dev-tool-core.js +1 -1
  13. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  14. package/dist/esm/dev-tool/index.d.ts.map +1 -1
  15. package/dist/esm/dev-tool/index.js +2 -8
  16. package/dist/esm/dev-tool/index.js.map +1 -1
  17. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  18. package/dist/esm/in-page-ui/dev-environment.d.ts +5 -0
  19. package/dist/esm/in-page-ui/dev-environment.d.ts.map +1 -0
  20. package/dist/esm/in-page-ui/dev-environment.js +18 -0
  21. package/dist/esm/in-page-ui/dev-environment.js.map +1 -0
  22. package/dist/esm/in-page-ui/issue-card.d.ts +52 -0
  23. package/dist/esm/in-page-ui/issue-card.d.ts.map +1 -0
  24. package/dist/esm/in-page-ui/issue-card.js +392 -0
  25. package/dist/esm/in-page-ui/issue-card.js.map +1 -0
  26. package/dist/esm/in-page-ui/issue-card.test.d.ts +1 -0
  27. package/dist/esm/in-page-ui/issue-card.test.js +60 -0
  28. package/dist/esm/in-page-ui/issue-card.test.js.map +1 -0
  29. package/dist/esm/in-page-ui/logo.d.ts +5 -0
  30. package/dist/esm/in-page-ui/logo.d.ts.map +1 -0
  31. package/dist/esm/in-page-ui/logo.js +6 -0
  32. package/dist/esm/in-page-ui/logo.js.map +1 -0
  33. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
  34. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  35. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +140 -15
  36. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  37. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  38. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +95 -12
  39. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  40. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
  41. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +1 -0
  42. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -1
  43. package/dist/esm/pushed-config-error-overlay/index.d.ts.map +1 -1
  44. package/dist/esm/pushed-config-error-overlay/index.js +42 -343
  45. package/dist/esm/pushed-config-error-overlay/index.js.map +1 -1
  46. package/dist/esm/setup-error-overlay/index.d.ts +16 -0
  47. package/dist/esm/setup-error-overlay/index.d.ts.map +1 -0
  48. package/dist/esm/setup-error-overlay/index.js +106 -0
  49. package/dist/esm/setup-error-overlay/index.js.map +1 -0
  50. package/dist/esm/setup-error-overlay/index.test.d.ts +1 -0
  51. package/dist/esm/setup-error-overlay/index.test.js +125 -0
  52. package/dist/esm/setup-error-overlay/index.test.js.map +1 -0
  53. package/dist/generated/quetzal-translations.d.ts +1 -1
  54. package/dist/{hexclave-provider-BKxi3Vy-.d.ts → hexclave-provider-BA97jMlx.d.ts} +2 -2
  55. package/dist/{hexclave-provider-BKxi3Vy-.d.ts.map → hexclave-provider-BA97jMlx.d.ts.map} +1 -1
  56. package/dist/in-page-ui/dev-environment.d.ts +5 -0
  57. package/dist/in-page-ui/dev-environment.d.ts.map +1 -0
  58. package/dist/in-page-ui/dev-environment.js +19 -0
  59. package/dist/in-page-ui/dev-environment.js.map +1 -0
  60. package/dist/in-page-ui/issue-card.d.ts +52 -0
  61. package/dist/in-page-ui/issue-card.d.ts.map +1 -0
  62. package/dist/in-page-ui/issue-card.js +397 -0
  63. package/dist/in-page-ui/issue-card.js.map +1 -0
  64. package/dist/in-page-ui/issue-card.test.d.ts +1 -0
  65. package/dist/in-page-ui/issue-card.test.js +59 -0
  66. package/dist/in-page-ui/issue-card.test.js.map +1 -0
  67. package/dist/in-page-ui/logo.d.ts +5 -0
  68. package/dist/in-page-ui/logo.d.ts.map +1 -0
  69. package/dist/in-page-ui/logo.js +7 -0
  70. package/dist/in-page-ui/logo.js.map +1 -0
  71. package/dist/index.d.ts +1 -1
  72. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
  73. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +140 -15
  74. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  75. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
  76. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +94 -11
  77. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  78. package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
  79. package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
  80. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +1 -0
  81. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -1
  82. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  83. package/dist/providers/hexclave-provider.d.ts +1 -1
  84. package/dist/providers/translation-provider.d.ts +1 -1
  85. package/dist/pushed-config-error-overlay/index.d.ts.map +1 -1
  86. package/dist/pushed-config-error-overlay/index.js +41 -342
  87. package/dist/pushed-config-error-overlay/index.js.map +1 -1
  88. package/dist/{quetzal-translations-XeH3zQ9l.d.ts → quetzal-translations-CK45_4Dm.d.ts} +3 -3
  89. package/dist/{quetzal-translations-XeH3zQ9l.d.ts.map → quetzal-translations-CK45_4Dm.d.ts.map} +1 -1
  90. package/dist/setup-error-overlay/index.d.ts +16 -0
  91. package/dist/setup-error-overlay/index.d.ts.map +1 -0
  92. package/dist/setup-error-overlay/index.js +108 -0
  93. package/dist/setup-error-overlay/index.js.map +1 -0
  94. package/dist/setup-error-overlay/index.test.d.ts +1 -0
  95. package/dist/setup-error-overlay/index.test.js +124 -0
  96. package/dist/setup-error-overlay/index.test.js.map +1 -0
  97. package/dist/{translation-provider-WOaZXqAz.d.ts → translation-provider-DQ3BJepJ.d.ts} +2 -2
  98. package/dist/{translation-provider-WOaZXqAz.d.ts.map → translation-provider-DQ3BJepJ.d.ts.map} +1 -1
  99. package/package.json +4 -4
  100. package/src/dev-tool/dev-tool-core.ts +1 -4
  101. package/src/dev-tool/index.ts +4 -15
  102. package/src/in-page-ui/dev-environment.ts +33 -0
  103. package/src/in-page-ui/issue-card.test.ts +67 -0
  104. package/src/in-page-ui/issue-card.ts +500 -0
  105. package/src/in-page-ui/logo.ts +8 -0
  106. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +158 -0
  107. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +125 -17
  108. package/src/lib/hexclave-app/apps/implementations/redirect-page-urls.test.ts +1 -0
  109. package/src/pushed-config-error-overlay/index.ts +45 -386
  110. package/src/setup-error-overlay/index.test.ts +133 -0
  111. package/src/setup-error-overlay/index.ts +150 -0
@@ -5,258 +5,21 @@
5
5
 
6
6
  import { captureError } from "@hexclave/shared/dist/utils/errors";
7
7
  import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
8
- import { isLocalhost } from "@hexclave/shared/dist/utils/urls";
9
- import { envVars } from "../generated/env";
10
- import { getInPageUiBaseCSS } from "../in-page-ui/base-styles";
11
- import { canMountIntoDom, getGlobalUiInstance, h, setGlobalUiInstance, setHtml } from "../in-page-ui/dom";
8
+ import { isLikelyDevelopmentEnvironment } from "../in-page-ui/dev-environment";
9
+ import { canMountIntoDom, getGlobalUiInstance, h, setGlobalUiInstance } from "../in-page-ui/dom";
10
+ import { getIssueCardCSS, renderIssueCard, renderIssuePill, trapFocusInIssueCard, type IssueCardFocusTrap } from "../in-page-ui/issue-card";
12
11
  import type { StackClientApp } from "../lib/hexclave-app";
13
12
 
14
13
  const GLOBAL_INSTANCE_KEY = "__hexclave-pushed-config-error-overlay";
15
14
  const MINIMIZED_STORAGE_KEY = "hexclave-pushed-config-error-minimized-key";
16
15
  const REFRESH_INTERVAL_MS = 5_000;
17
- const HEXCLAVE_LOGO_SVG = '<svg width="16" height="16" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="miter"><path d="M 24 4 L 41.32 14 L 41.32 34 L 24 44 L 6.68 34 L 6.68 14 Z"/><path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="currentColor" stroke="none"/><path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="currentColor" stroke="none" transform="rotate(120 24 24)"/><path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="currentColor" stroke="none" transform="rotate(240 24 24)"/></svg>';
18
- const COPY_ICON_SVG = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
19
16
 
20
17
  type ConfigIssue = {
21
18
  kind: "error" | "warning",
22
19
  messages: string[],
23
20
  };
24
21
 
25
- const css = getInPageUiBaseCSS(".hexclave-config-error-overlay") + `
26
- .hexclave-config-error-overlay .hce-backdrop {
27
- position: fixed;
28
- inset: 0;
29
- z-index: 2147483647;
30
- display: flex;
31
- align-items: center;
32
- justify-content: center;
33
- padding: 24px;
34
- background: rgba(0, 0, 0, 0.46);
35
- backdrop-filter: blur(6px);
36
- overflow: auto;
37
- }
38
-
39
- .hexclave-config-error-overlay .hce-card {
40
- --hce-status: var(--sdt-error);
41
- width: min(720px, calc(100vw - 32px));
42
- max-height: min(640px, calc(100dvh - 48px));
43
- border: 1px solid color-mix(in srgb, var(--hce-status) 35%, var(--sdt-border));
44
- border-radius: 18px;
45
- background: var(--sdt-overlay-bg);
46
- box-shadow: var(--sdt-shadow);
47
- backdrop-filter: blur(18px);
48
- display: flex;
49
- overflow: hidden;
50
- }
51
-
52
- .hexclave-config-error-overlay .hce-card-warning {
53
- --hce-status: var(--sdt-warning);
54
- }
55
-
56
- .hexclave-config-error-overlay .hce-card-inner {
57
- padding: 18px;
58
- width: 100%;
59
- overflow: auto;
60
- }
61
-
62
- .hexclave-config-error-overlay .hce-header {
63
- display: flex;
64
- align-items: flex-start;
65
- justify-content: space-between;
66
- gap: 12px;
67
- margin-bottom: 12px;
68
- }
69
-
70
- .hexclave-config-error-overlay .hce-title-row {
71
- display: flex;
72
- align-items: flex-start;
73
- gap: 10px;
74
- min-width: 0;
75
- }
76
-
77
- .hexclave-config-error-overlay .hce-logo {
78
- flex-shrink: 0;
79
- display: flex;
80
- align-items: center;
81
- justify-content: center;
82
- width: 34px;
83
- height: 34px;
84
- border-radius: 10px;
85
- background: var(--hce-status);
86
- color: white;
87
- box-shadow: 0 10px 30px color-mix(in srgb, var(--hce-status) 32%, transparent);
88
- }
89
-
90
- .hexclave-config-error-overlay .hce-badge {
91
- display: inline-flex;
92
- flex-shrink: 0;
93
- padding: 2px 6px;
94
- border-radius: 999px;
95
- background: var(--hce-status);
96
- color: white;
97
- font-size: 10px;
98
- font-weight: 700;
99
- letter-spacing: 0.06em;
100
- text-transform: uppercase;
101
- }
102
-
103
- .hexclave-config-error-overlay .hce-title {
104
- color: var(--sdt-text);
105
- margin-top: 4px;
106
- font-size: 18px;
107
- font-weight: 700;
108
- line-height: 1.25;
109
- }
110
-
111
- .hexclave-config-error-overlay .hce-actions {
112
- display: flex;
113
- gap: 4px;
114
- }
115
-
116
- .hexclave-config-error-overlay .hce-icon-btn {
117
- display: inline-flex;
118
- align-items: center;
119
- justify-content: center;
120
- width: 28px;
121
- height: 28px;
122
- border: 1px solid var(--sdt-border);
123
- border-radius: 8px;
124
- background: var(--sdt-bg-elevated);
125
- color: var(--sdt-text-secondary);
126
- cursor: pointer;
127
- font: inherit;
128
- line-height: 1;
129
- vertical-align: top;
130
- }
131
-
132
- .hexclave-config-error-overlay .hce-icon-btn svg {
133
- display: block;
134
- flex-shrink: 0;
135
- }
136
-
137
- .hexclave-config-error-overlay .hce-text-btn {
138
- align-items: center;
139
- gap: 6px;
140
- min-height: 28px;
141
- padding: 0 10px;
142
- width: auto;
143
- font-size: 12px;
144
- line-height: 1;
145
- }
146
-
147
- .hexclave-config-error-overlay .hce-icon-btn:hover {
148
- background: var(--sdt-bg-hover);
149
- color: var(--sdt-text);
150
- }
151
-
152
- .hexclave-config-error-overlay .hce-body {
153
- color: var(--sdt-text-secondary);
154
- font-size: 14px;
155
- line-height: 1.5;
156
- }
157
-
158
- .hexclave-config-error-overlay .hce-message-header {
159
- display: flex;
160
- align-items: center;
161
- justify-content: space-between;
162
- gap: 10px;
163
- margin-top: 14px;
164
- margin-bottom: 8px;
165
- }
166
-
167
- .hexclave-config-error-overlay .hce-message-label {
168
- color: var(--sdt-text);
169
- font-size: 12px;
170
- font-weight: 650;
171
- }
172
-
173
- .hexclave-config-error-overlay .hce-message {
174
- padding: 12px;
175
- max-height: min(260px, max(96px, 30dvh));
176
- overflow: auto;
177
- border: 1px solid var(--sdt-border-subtle);
178
- border-radius: 10px;
179
- background: var(--sdt-bg-subtle);
180
- color: var(--sdt-text);
181
- font-family: var(--sdt-font-mono);
182
- font-size: 12px;
183
- white-space: pre-wrap;
184
- overflow-wrap: anywhere;
185
- }
186
-
187
- .hexclave-config-error-overlay .hce-footer {
188
- margin-top: 10px;
189
- color: var(--sdt-text-tertiary);
190
- font-size: 12px;
191
- }
192
-
193
- .hexclave-config-error-overlay .hce-pill {
194
- position: fixed;
195
- right: 18px;
196
- bottom: 18px;
197
- z-index: 2147483647;
198
- display: flex;
199
- align-items: center;
200
- gap: 8px;
201
- padding: 8px 12px 8px 8px;
202
- --hce-status: var(--sdt-error);
203
- border: 1px solid color-mix(in srgb, var(--hce-status) 35%, var(--sdt-border));
204
- border-radius: 999px;
205
- background: var(--sdt-overlay-bg);
206
- box-shadow: var(--sdt-trigger-shadow);
207
- color: var(--sdt-text);
208
- cursor: pointer;
209
- font: inherit;
210
- backdrop-filter: blur(18px);
211
- }
212
-
213
- .hexclave-config-error-overlay .hce-pill-warning {
214
- --hce-status: var(--sdt-warning);
215
- }
216
-
217
- .hexclave-config-error-overlay .hce-pill-logo {
218
- display: flex;
219
- align-items: center;
220
- justify-content: center;
221
- width: 26px;
222
- height: 26px;
223
- border-radius: 999px;
224
- background: var(--hce-status);
225
- color: white;
226
- }
227
-
228
- @media (max-height: 520px) {
229
- .hexclave-config-error-overlay .hce-backdrop {
230
- align-items: flex-start;
231
- padding: 12px;
232
- }
233
-
234
- .hexclave-config-error-overlay .hce-card {
235
- width: min(720px, calc(100vw - 24px));
236
- max-height: calc(100dvh - 24px);
237
- }
238
-
239
- .hexclave-config-error-overlay .hce-card-inner {
240
- padding: 12px;
241
- }
242
-
243
- .hexclave-config-error-overlay .hce-header {
244
- margin-bottom: 8px;
245
- }
246
-
247
- .hexclave-config-error-overlay .hce-title {
248
- font-size: 16px;
249
- }
250
-
251
- .hexclave-config-error-overlay .hce-body {
252
- font-size: 13px;
253
- }
254
-
255
- .hexclave-config-error-overlay .hce-message {
256
- max-height: max(80px, 24dvh);
257
- }
258
- }
259
- `;
22
+ const css = getIssueCardCSS(".hexclave-config-error-overlay");
260
23
 
261
24
  function storageGet(key: string): string | null {
262
25
  try {
@@ -283,53 +46,7 @@ function storageRemove(key: string): void {
283
46
  }
284
47
 
285
48
  function shouldMount(): boolean {
286
- if (!canMountIntoDom()) {
287
- return false;
288
- }
289
-
290
- const nodeEnv = envVars.NODE_ENV;
291
- if (nodeEnv !== undefined) {
292
- return nodeEnv === "development";
293
- }
294
-
295
- try {
296
- const url = new URL(window.location.href);
297
- if (url.protocol === "file:") {
298
- return true;
299
- }
300
- } catch {
301
- return false;
302
- }
303
- return isLocalhost(window.location.href);
304
- }
305
-
306
- async function copyTextToClipboard(text: string): Promise<void> {
307
- const clipboard: unknown = Reflect.get(navigator, "clipboard");
308
- const writeText = clipboard != null && typeof clipboard === "object"
309
- ? Reflect.get(clipboard, "writeText")
310
- : null;
311
- if (typeof writeText === "function") {
312
- await writeText.call(clipboard, text);
313
- return;
314
- }
315
-
316
- const textarea = h("textarea", {
317
- style: {
318
- position: "fixed",
319
- left: "-9999px",
320
- top: "0",
321
- opacity: "0",
322
- },
323
- readonly: "true",
324
- }) as HTMLTextAreaElement;
325
- textarea.value = text;
326
- document.body.appendChild(textarea);
327
- textarea.select();
328
- const copied = document.execCommand("copy");
329
- textarea.remove();
330
- if (!copied) {
331
- throw new Error("Browser refused to copy the config error message.");
332
- }
49
+ return canMountIntoDom() && isLikelyDevelopmentEnvironment();
333
50
  }
334
51
 
335
52
  function buildConfigIssueAiPrompt(issue: ConfigIssue): string {
@@ -363,12 +80,23 @@ export function mountPushedConfigErrorOverlay(app: StackClientApp<true>): () =>
363
80
  let lastErrorKey: string | null = null;
364
81
  let lastConsoleErrorKey: string | null = null;
365
82
  let minimized = false;
83
+ // The card element is replaced on every poll, so the trap has to be rebound to the new one each time or it would keep
84
+ // trapping Tab inside the detached card. Focus is only *moved* into the card when a card first appears for an issue;
85
+ // doing it on every render would yank focus out of whatever the developer is doing every few seconds.
86
+ let focusedIssueKey: string | null = null;
87
+ let focusTrap: IssueCardFocusTrap | null = null;
88
+ const releaseFocusIfHeld = () => {
89
+ focusTrap?.release();
90
+ focusTrap = null;
91
+ focusedIssueKey = null;
92
+ };
366
93
 
367
94
  const render = (issue: ConfigIssue | null) => {
368
95
  root.replaceChildren(style);
369
96
  if (issue == null) {
370
97
  lastErrorKey = null;
371
98
  minimized = false;
99
+ releaseFocusIfHeld();
372
100
  return;
373
101
  }
374
102
 
@@ -400,113 +128,43 @@ export function mountPushedConfigErrorOverlay(app: StackClientApp<true>): () =>
400
128
  }
401
129
 
402
130
  if (minimized) {
403
- const logoSpan = h("span", { className: "hce-pill-logo" });
404
- setHtml(logoSpan, HEXCLAVE_LOGO_SVG);
405
- root.appendChild(h("button", {
406
- className: issue.kind === "error" ? "hce-pill" : "hce-pill hce-pill-warning",
407
- type: "button",
131
+ releaseFocusIfHeld();
132
+ root.appendChild(renderIssuePill({
133
+ kind: issue.kind,
134
+ label: issue.kind === "error" ? "Config error" : "Config warning",
135
+ ariaLabel: `Show Hexclave config ${issueLabel}`,
408
136
  onClick: () => {
409
137
  minimized = false;
410
138
  storageRemove(MINIMIZED_STORAGE_KEY);
411
139
  render(issue);
412
140
  },
413
- },
414
- logoSpan,
415
- h("span", null, issue.kind === "error" ? "Config error" : "Config warning")));
141
+ }));
416
142
  return;
417
143
  }
418
144
 
419
- const logoSpan = h("span", { className: "hce-logo" });
420
- setHtml(logoSpan, HEXCLAVE_LOGO_SVG);
421
- const copyButton = h("button", {
422
- className: "hce-icon-btn hce-text-btn",
423
- type: "button",
424
- title: issue.kind === "error" ? "Copy error message" : "Copy warning message",
425
- "aria-label": issue.kind === "error" ? "Copy config error message" : "Copy config warning message",
426
- onClick: () => {
427
- runAsynchronously(async () => {
428
- await copyTextToClipboard(issueMessage);
429
- copyButton.textContent = "Copied";
430
- setTimeout(() => {
431
- setHtml(copyButton, `${COPY_ICON_SVG}Copy`);
432
- }, 1500);
433
- }, {
434
- noErrorLogging: true,
435
- onError: (copyError) => {
436
- captureError("pushed-config-error-overlay-copy", copyError);
437
- copyButton.textContent = "Copy failed";
438
- setTimeout(() => {
439
- setHtml(copyButton, `${COPY_ICON_SVG}Copy`);
440
- }, 1500);
441
- },
442
- });
145
+ root.appendChild(renderIssueCard({
146
+ kind: issue.kind,
147
+ badge: `Config ${issueLabel}`,
148
+ title: issueTitle,
149
+ bodyText,
150
+ messageLabel: issue.kind === "error" ? "Error message" : "Warning message",
151
+ message: issueMessage,
152
+ footerText,
153
+ ariaLabel: `Hexclave config ${issueLabel}`,
154
+ aiPrompt: buildConfigIssueAiPrompt(issue),
155
+ onCopyError: (copyError) => captureError("pushed-config-error-overlay-copy", copyError),
156
+ onCopyAiPromptError: (copyError) => captureError("pushed-config-error-overlay-copy-ai-prompt", copyError),
157
+ onMinimize: () => {
158
+ minimized = true;
159
+ storageSet(MINIMIZED_STORAGE_KEY, issueKey);
160
+ render(issue);
443
161
  },
444
- });
445
- setHtml(copyButton, `${COPY_ICON_SVG}Copy`);
446
-
447
- const aiPromptCopyButton = h("button", {
448
- className: "hce-icon-btn",
449
- type: "button",
450
- title: "Copy AI prompt",
451
- "aria-label": issue.kind === "error" ? "Copy AI prompt for config error" : "Copy AI prompt for config warning",
452
- onClick: () => {
453
- runAsynchronously(async () => {
454
- await copyTextToClipboard(buildConfigIssueAiPrompt(issue));
455
- aiPromptCopyButton.textContent = "✓";
456
- setTimeout(() => {
457
- setHtml(aiPromptCopyButton, COPY_ICON_SVG);
458
- }, 1500);
459
- }, {
460
- noErrorLogging: true,
461
- onError: (copyError) => {
462
- captureError("pushed-config-error-overlay-copy-ai-prompt", copyError);
463
- aiPromptCopyButton.textContent = "!";
464
- setTimeout(() => {
465
- setHtml(aiPromptCopyButton, COPY_ICON_SVG);
466
- }, 1500);
467
- },
468
- });
469
- },
470
- });
471
- setHtml(aiPromptCopyButton, COPY_ICON_SVG);
472
-
473
- root.appendChild(h("div", { className: "hce-backdrop" },
474
- h("div", { className: issue.kind === "error" ? "hce-card" : "hce-card hce-card-warning", role: "alertdialog", "aria-modal": "true", "aria-label": `Hexclave config ${issueLabel}` },
475
- h("div", { className: "hce-card-inner" },
476
- h("div", { className: "hce-header" },
477
- h("div", { className: "hce-title-row" },
478
- logoSpan,
479
- h("div", null,
480
- h("span", { className: "hce-badge" }, `Config ${issueLabel}`),
481
- h("div", { className: "hce-title" }, issueTitle),
482
- ),
483
- ),
484
- h("div", { className: "hce-actions" },
485
- aiPromptCopyButton,
486
- h("button", {
487
- className: "hce-icon-btn",
488
- type: "button",
489
- title: "Minimize",
490
- "aria-label": issue.kind === "error" ? "Minimize config error" : "Minimize config warning",
491
- onClick: () => {
492
- minimized = true;
493
- storageSet(MINIMIZED_STORAGE_KEY, issueKey);
494
- render(issue);
495
- },
496
- }, "–"),
497
- ),
498
- ),
499
- h("div", { className: "hce-body" },
500
- bodyText,
501
- h("div", { className: "hce-message-header" },
502
- h("div", { className: "hce-message-label" }, issue.kind === "error" ? "Error message" : "Warning message"),
503
- copyButton,
504
- ),
505
- h("div", { className: "hce-message" }, issueMessage),
506
- h("div", { className: "hce-footer" }, footerText),
507
- ),
508
- ),
509
- )));
162
+ }));
163
+ const isNewIssue = focusedIssueKey !== issueKey;
164
+ const previouslyFocused = isNewIssue ? undefined : focusTrap?.previouslyFocused ?? null;
165
+ focusTrap?.release({ restoreFocus: isNewIssue });
166
+ focusedIssueKey = issueKey;
167
+ focusTrap = trapFocusInIssueCard(root, { moveFocusIntoCard: isNewIssue, previouslyFocused });
510
168
  };
511
169
 
512
170
  const refresh = () => {
@@ -540,6 +198,7 @@ export function mountPushedConfigErrorOverlay(app: StackClientApp<true>): () =>
540
198
  const cleanup = () => {
541
199
  disposed = true;
542
200
  clearInterval(interval);
201
+ releaseFocusIfHeld();
543
202
  root.remove();
544
203
  if (getGlobalUiInstance(GLOBAL_INSTANCE_KEY)?.cleanup === cleanup) {
545
204
  setGlobalUiInstance(GLOBAL_INSTANCE_KEY, null);
@@ -0,0 +1,133 @@
1
+
2
+ //===========================================
3
+ // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
+ //===========================================
5
+ // @vitest-environment jsdom
6
+
7
+ import { HexclaveSetupError } from "@hexclave/shared/dist/utils/errors";
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+ import { SETUP_ERROR_OVERLAY_GLOBAL_INSTANCE_KEY, showSetupErrorOverlay } from ".";
10
+
11
+ function createSetupError() {
12
+ return new HexclaveSetupError({
13
+ title: "A domain in your authentication flow is not one of your project's trusted domains",
14
+ message: "Nested cross-domain auth callback URL https://demo.example.test/ is not trusted.",
15
+ howToFix: ["Add https://demo.example.test to your project's trusted domains."],
16
+ });
17
+ }
18
+
19
+ function overlayRoots() {
20
+ return document.querySelectorAll(".hexclave-setup-error-overlay");
21
+ }
22
+
23
+ describe("setup error overlay", () => {
24
+ afterEach(() => {
25
+ vi.unstubAllEnvs();
26
+ for (const root of overlayRoots()) {
27
+ root.remove();
28
+ }
29
+ Reflect.deleteProperty(window, SETUP_ERROR_OVERLAY_GLOBAL_INSTANCE_KEY);
30
+ });
31
+
32
+ it("renders setup errors with their fix instructions, even outside development", () => {
33
+ vi.stubEnv("NODE_ENV", "production");
34
+ const error = createSetupError();
35
+
36
+ const cleanup = showSetupErrorOverlay(error);
37
+ try {
38
+ expect(overlayRoots()).toHaveLength(1);
39
+ const text = document.body.textContent;
40
+ expect(text).toContain(error.title);
41
+ expect(text).toContain(error.message);
42
+ expect(text).toContain(error.howToFix[0]);
43
+ expect(document.querySelector("[role='alertdialog']")).not.toBeNull();
44
+ } finally {
45
+ cleanup();
46
+ }
47
+
48
+ expect(overlayRoots()).toHaveLength(0);
49
+ });
50
+
51
+ it("minimizes into a pill and back", () => {
52
+ vi.stubEnv("NODE_ENV", "production");
53
+ const cleanup = showSetupErrorOverlay(createSetupError());
54
+ try {
55
+ const minimizeButton = document.querySelector<HTMLButtonElement>("[aria-label='Minimize Hexclave setup error']");
56
+ expect(minimizeButton).not.toBeNull();
57
+ minimizeButton?.click();
58
+ expect(document.querySelector("[role='alertdialog']")).toBeNull();
59
+
60
+ const pill = document.querySelector<HTMLButtonElement>("[aria-label='Show Hexclave setup error']");
61
+ expect(pill).not.toBeNull();
62
+ pill?.click();
63
+ expect(document.querySelector("[role='alertdialog']")).not.toBeNull();
64
+ } finally {
65
+ cleanup();
66
+ }
67
+ });
68
+
69
+ it("keeps keyboard focus inside the card and gives it back afterwards", () => {
70
+ vi.stubEnv("NODE_ENV", "production");
71
+ const pageButton = document.body.appendChild(document.createElement("button"));
72
+ pageButton.focus();
73
+
74
+ const cleanup = showSetupErrorOverlay(createSetupError());
75
+ try {
76
+ const card = document.querySelector<HTMLElement>("[role='alertdialog']");
77
+ expect(document.activeElement).toBe(card);
78
+
79
+ const cardButtons = [...(card?.querySelectorAll("button") ?? [])];
80
+ expect(cardButtons.length).toBeGreaterThan(1);
81
+ cardButtons[cardButtons.length - 1].focus();
82
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true }));
83
+ expect(document.activeElement).toBe(cardButtons[0]);
84
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true, shiftKey: true }));
85
+ expect(document.activeElement).toBe(cardButtons[cardButtons.length - 1]);
86
+
87
+ // Tabbing away from the card is what the trap exists to prevent, even when the page itself moves focus.
88
+ pageButton.focus();
89
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true }));
90
+ expect(document.activeElement).toBe(cardButtons[0]);
91
+ } finally {
92
+ cleanup();
93
+ }
94
+
95
+ expect(document.activeElement).toBe(pageButton);
96
+ pageButton.remove();
97
+ });
98
+
99
+ it("keeps the first card instead of stacking later errors on top of it", () => {
100
+ vi.stubEnv("NODE_ENV", "production");
101
+ const firstError = createSetupError();
102
+ const cleanup = showSetupErrorOverlay(firstError);
103
+ try {
104
+ const secondCleanup = showSetupErrorOverlay(new HexclaveSetupError({
105
+ title: "Another setup error",
106
+ message: "Another setup error happened.",
107
+ howToFix: ["Fix the other thing."],
108
+ }));
109
+ secondCleanup();
110
+
111
+ expect(overlayRoots()).toHaveLength(1);
112
+ expect(document.body.textContent).toContain(firstError.message);
113
+ expect(document.body.textContent).not.toContain("Another setup error happened.");
114
+ } finally {
115
+ cleanup();
116
+ }
117
+ });
118
+
119
+ it("only shows errors that are not setup errors while developing", () => {
120
+ vi.stubEnv("NODE_ENV", "production");
121
+ showSetupErrorOverlay(new Error("Some internal failure."))();
122
+ expect(overlayRoots()).toHaveLength(0);
123
+
124
+ vi.stubEnv("NODE_ENV", "development");
125
+ const cleanup = showSetupErrorOverlay(new Error("Some internal failure."));
126
+ try {
127
+ expect(overlayRoots()).toHaveLength(1);
128
+ expect(document.body.textContent).toContain("Some internal failure.");
129
+ } finally {
130
+ cleanup();
131
+ }
132
+ });
133
+ });