@hexclave/next 1.0.75 → 1.0.77

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 (231) hide show
  1. package/dist/{admin-app-impl-sTQD2d-3.d.ts → admin-app-impl-CxDpE89P.d.ts} +12 -10
  2. package/dist/admin-app-impl-CxDpE89P.d.ts.map +1 -0
  3. package/dist/clickmap/clickmap-core.d.ts +1 -1
  4. package/dist/clickmap/index.d.ts +1 -1
  5. package/dist/{client-app-BCT5p_70.d.ts → client-app-wFodsYml.d.ts} +4 -1
  6. package/dist/client-app-wFodsYml.d.ts.map +1 -0
  7. package/dist/common-BK_sydxF.d.ts.map +1 -1
  8. package/dist/components/elements/sidebar-layout.d.ts.map +1 -1
  9. package/dist/components/elements/sidebar-layout.js +12 -2
  10. package/dist/components/elements/sidebar-layout.js.map +1 -1
  11. package/dist/components/selected-team-switcher.d.ts +1 -1
  12. package/dist/components/team-icon.d.ts +1 -1
  13. package/dist/components/team-switcher.d.ts +1 -1
  14. package/dist/components-page/account-settings/payments/payments-page.d.ts +1 -1
  15. package/dist/components-page/account-settings/teams/leave-team-section.d.ts +1 -1
  16. package/dist/components-page/account-settings/teams/team-api-keys-section.d.ts +1 -1
  17. package/dist/components-page/account-settings/teams/team-display-name-section.d.ts +1 -1
  18. package/dist/components-page/account-settings/teams/team-member-invitation-section.d.ts +1 -1
  19. package/dist/components-page/account-settings/teams/team-member-list-section.d.ts +1 -1
  20. package/dist/components-page/account-settings/teams/team-page.d.ts +1 -1
  21. package/dist/components-page/account-settings/teams/team-profile-image-section.d.ts +1 -1
  22. package/dist/components-page/account-settings/teams/team-profile-user-section.d.ts +1 -1
  23. package/dist/components-page/hexclave-handler-client.d.ts +1 -1
  24. package/dist/components-page/hexclave-handler.d.ts +1 -1
  25. package/dist/components-page/team-invitation-cache.d.ts +5 -0
  26. package/dist/components-page/team-invitation-cache.d.ts.map +1 -0
  27. package/dist/components-page/team-invitation-cache.js +12 -0
  28. package/dist/components-page/team-invitation-cache.js.map +1 -0
  29. package/dist/components-page/team-invitation-cache.test.d.ts +1 -0
  30. package/dist/components-page/team-invitation-cache.test.js +23 -0
  31. package/dist/components-page/team-invitation-cache.test.js.map +1 -0
  32. package/dist/components-page/team-invitation.js +8 -7
  33. package/dist/components-page/team-invitation.js.map +1 -1
  34. package/dist/dev-tool/dev-tool-core.d.ts +1 -1
  35. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  36. package/dist/dev-tool/dev-tool-core.js +2 -2
  37. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  38. package/dist/dev-tool/index.d.ts +1 -1
  39. package/dist/dev-tool/index.d.ts.map +1 -1
  40. package/dist/dev-tool/index.js +2 -8
  41. package/dist/dev-tool/index.js.map +1 -1
  42. package/dist/esm/components/elements/sidebar-layout.d.ts.map +1 -1
  43. package/dist/esm/components/elements/sidebar-layout.js +12 -2
  44. package/dist/esm/components/elements/sidebar-layout.js.map +1 -1
  45. package/dist/esm/components-page/team-invitation-cache.d.ts +5 -0
  46. package/dist/esm/components-page/team-invitation-cache.d.ts.map +1 -0
  47. package/dist/esm/components-page/team-invitation-cache.js +11 -0
  48. package/dist/esm/components-page/team-invitation-cache.js.map +1 -0
  49. package/dist/esm/components-page/team-invitation-cache.test.d.ts +1 -0
  50. package/dist/esm/components-page/team-invitation-cache.test.js +24 -0
  51. package/dist/esm/components-page/team-invitation-cache.test.js.map +1 -0
  52. package/dist/esm/components-page/team-invitation.d.ts.map +1 -1
  53. package/dist/esm/components-page/team-invitation.js +8 -7
  54. package/dist/esm/components-page/team-invitation.js.map +1 -1
  55. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  56. package/dist/esm/dev-tool/dev-tool-core.js +1 -1
  57. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  58. package/dist/esm/dev-tool/index.d.ts.map +1 -1
  59. package/dist/esm/dev-tool/index.js +2 -8
  60. package/dist/esm/dev-tool/index.js.map +1 -1
  61. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  62. package/dist/esm/in-page-ui/dev-environment.d.ts +5 -0
  63. package/dist/esm/in-page-ui/dev-environment.d.ts.map +1 -0
  64. package/dist/esm/in-page-ui/dev-environment.js +18 -0
  65. package/dist/esm/in-page-ui/dev-environment.js.map +1 -0
  66. package/dist/esm/in-page-ui/issue-card.d.ts +52 -0
  67. package/dist/esm/in-page-ui/issue-card.d.ts.map +1 -0
  68. package/dist/esm/in-page-ui/issue-card.js +392 -0
  69. package/dist/esm/in-page-ui/issue-card.js.map +1 -0
  70. package/dist/esm/in-page-ui/issue-card.test.d.ts +1 -0
  71. package/dist/esm/in-page-ui/issue-card.test.js +60 -0
  72. package/dist/esm/in-page-ui/issue-card.test.js.map +1 -0
  73. package/dist/esm/in-page-ui/logo.d.ts +5 -0
  74. package/dist/esm/in-page-ui/logo.d.ts.map +1 -0
  75. package/dist/esm/in-page-ui/logo.js +6 -0
  76. package/dist/esm/in-page-ui/logo.js.map +1 -0
  77. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +4 -4
  78. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  79. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +30 -1
  80. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -1
  81. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +140 -15
  82. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  83. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +5 -3
  84. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  85. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +184 -28
  86. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  87. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.d.ts +1 -0
  88. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js +83 -0
  89. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js.map +1 -0
  90. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.d.ts +1 -0
  91. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js +120 -0
  92. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js.map +1 -0
  93. package/dist/esm/lib/hexclave-app/apps/implementations/common.d.ts.map +1 -1
  94. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +4 -3
  95. package/dist/esm/lib/hexclave-app/apps/implementations/common.js.map +1 -1
  96. package/dist/esm/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
  97. package/dist/esm/lib/hexclave-app/apps/implementations/index.js +1 -1
  98. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +1 -0
  99. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -1
  100. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +2 -2
  101. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
  102. package/dist/esm/lib/hexclave-app/teams/index.d.ts +3 -0
  103. package/dist/esm/lib/hexclave-app/teams/index.d.ts.map +1 -1
  104. package/dist/esm/lib/hexclave-app/teams/index.js.map +1 -1
  105. package/dist/esm/pushed-config-error-overlay/index.d.ts.map +1 -1
  106. package/dist/esm/pushed-config-error-overlay/index.js +42 -343
  107. package/dist/esm/pushed-config-error-overlay/index.js.map +1 -1
  108. package/dist/esm/setup-error-overlay/index.d.ts +16 -0
  109. package/dist/esm/setup-error-overlay/index.d.ts.map +1 -0
  110. package/dist/esm/setup-error-overlay/index.js +106 -0
  111. package/dist/esm/setup-error-overlay/index.js.map +1 -0
  112. package/dist/esm/setup-error-overlay/index.test.d.ts +1 -0
  113. package/dist/esm/setup-error-overlay/index.test.js +125 -0
  114. package/dist/esm/setup-error-overlay/index.test.js.map +1 -0
  115. package/dist/esm/utils/monkey-patch.d.ts +7 -0
  116. package/dist/esm/utils/monkey-patch.d.ts.map +1 -0
  117. package/dist/esm/utils/monkey-patch.js +20 -0
  118. package/dist/esm/utils/monkey-patch.js.map +1 -0
  119. package/dist/esm/utils/react.d.ts +17 -0
  120. package/dist/esm/utils/react.d.ts.map +1 -0
  121. package/dist/esm/utils/react.js +60 -0
  122. package/dist/esm/utils/react.js.map +1 -0
  123. package/dist/generated/quetzal-translations.d.ts +1 -1
  124. package/dist/{hexclave-provider-BKxi3Vy-.d.ts → hexclave-provider-CMz2mt4I.d.ts} +3 -3
  125. package/dist/{hexclave-provider-BKxi3Vy-.d.ts.map → hexclave-provider-CMz2mt4I.d.ts.map} +1 -1
  126. package/dist/{hooks-CUe5bc3s.d.ts → hooks-CUHt4Y9C.d.ts} +2 -2
  127. package/dist/{hooks-CUe5bc3s.d.ts.map → hooks-CUHt4Y9C.d.ts.map} +1 -1
  128. package/dist/in-page-ui/dev-environment.d.ts +5 -0
  129. package/dist/in-page-ui/dev-environment.d.ts.map +1 -0
  130. package/dist/in-page-ui/dev-environment.js +19 -0
  131. package/dist/in-page-ui/dev-environment.js.map +1 -0
  132. package/dist/in-page-ui/issue-card.d.ts +52 -0
  133. package/dist/in-page-ui/issue-card.d.ts.map +1 -0
  134. package/dist/in-page-ui/issue-card.js +397 -0
  135. package/dist/in-page-ui/issue-card.js.map +1 -0
  136. package/dist/in-page-ui/issue-card.test.d.ts +1 -0
  137. package/dist/in-page-ui/issue-card.test.js +59 -0
  138. package/dist/in-page-ui/issue-card.test.js.map +1 -0
  139. package/dist/in-page-ui/logo.d.ts +5 -0
  140. package/dist/in-page-ui/logo.d.ts.map +1 -0
  141. package/dist/in-page-ui/logo.js +7 -0
  142. package/dist/in-page-ui/logo.js.map +1 -0
  143. package/dist/{index-CvB7XacB.d.ts → index-fR0LkUK7.d.ts} +2 -2
  144. package/dist/{index-CvB7XacB.d.ts.map → index-fR0LkUK7.d.ts.map} +1 -1
  145. package/dist/index.d.ts +4 -4
  146. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
  147. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +29 -0
  148. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -1
  149. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +140 -15
  150. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  151. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +2 -2
  152. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +182 -25
  153. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  154. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.d.ts +1 -0
  155. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js +82 -0
  156. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js.map +1 -0
  157. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.d.ts +1 -0
  158. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js +119 -0
  159. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js.map +1 -0
  160. package/dist/lib/hexclave-app/apps/implementations/common.js +4 -3
  161. package/dist/lib/hexclave-app/apps/implementations/common.js.map +1 -1
  162. package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
  163. package/dist/lib/hexclave-app/apps/implementations/index.js +1 -1
  164. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +1 -0
  165. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -1
  166. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  167. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
  168. package/dist/lib/hexclave-app/apps/index.d.ts +1 -1
  169. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -1
  170. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -1
  171. package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
  172. package/dist/lib/hexclave-app/index.d.ts +1 -1
  173. package/dist/lib/hexclave-app/projects/index.d.ts +1 -1
  174. package/dist/lib/hexclave-app/teams/index.d.ts +1 -1
  175. package/dist/lib/hexclave-app/teams/index.js.map +1 -1
  176. package/dist/lib/hexclave-app/users/index.d.ts +1 -1
  177. package/dist/lib/hooks.d.ts +1 -1
  178. package/dist/providers/hexclave-context.d.ts +1 -1
  179. package/dist/providers/hexclave-provider-client.d.ts +1 -1
  180. package/dist/providers/hexclave-provider.d.ts +1 -1
  181. package/dist/providers/translation-provider.d.ts +1 -1
  182. package/dist/pushed-config-error-overlay/index.d.ts +1 -1
  183. package/dist/pushed-config-error-overlay/index.d.ts.map +1 -1
  184. package/dist/pushed-config-error-overlay/index.js +41 -342
  185. package/dist/pushed-config-error-overlay/index.js.map +1 -1
  186. package/dist/{quetzal-translations-XeH3zQ9l.d.ts → quetzal-translations-B2jT2SWm.d.ts} +3 -3
  187. package/dist/{quetzal-translations-XeH3zQ9l.d.ts.map → quetzal-translations-B2jT2SWm.d.ts.map} +1 -1
  188. package/dist/setup-error-overlay/index.d.ts +16 -0
  189. package/dist/setup-error-overlay/index.d.ts.map +1 -0
  190. package/dist/setup-error-overlay/index.js +108 -0
  191. package/dist/setup-error-overlay/index.js.map +1 -0
  192. package/dist/setup-error-overlay/index.test.d.ts +1 -0
  193. package/dist/setup-error-overlay/index.test.js +124 -0
  194. package/dist/setup-error-overlay/index.test.js.map +1 -0
  195. package/dist/team-invitation-B5Ol7vdd.d.ts.map +1 -1
  196. package/dist/{translation-provider-WOaZXqAz.d.ts → translation-provider-6505O9P2.d.ts} +2 -2
  197. package/dist/{translation-provider-WOaZXqAz.d.ts.map → translation-provider-6505O9P2.d.ts.map} +1 -1
  198. package/dist/utils/monkey-patch.d.ts +7 -0
  199. package/dist/utils/monkey-patch.d.ts.map +1 -0
  200. package/dist/utils/monkey-patch.js +23 -0
  201. package/dist/utils/monkey-patch.js.map +1 -0
  202. package/dist/utils/react.d.ts +17 -0
  203. package/dist/utils/react.d.ts.map +1 -0
  204. package/dist/utils/react.js +63 -0
  205. package/dist/utils/react.js.map +1 -0
  206. package/package.json +4 -4
  207. package/src/components/elements/sidebar-layout.tsx +16 -1
  208. package/src/components-page/team-invitation-cache.test.ts +25 -0
  209. package/src/components-page/team-invitation-cache.ts +13 -0
  210. package/src/components-page/team-invitation.tsx +16 -7
  211. package/src/dev-tool/dev-tool-core.ts +1 -4
  212. package/src/dev-tool/index.ts +4 -15
  213. package/src/in-page-ui/dev-environment.ts +33 -0
  214. package/src/in-page-ui/issue-card.test.ts +67 -0
  215. package/src/in-page-ui/issue-card.ts +500 -0
  216. package/src/in-page-ui/logo.ts +8 -0
  217. package/src/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.ts +38 -1
  218. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +157 -0
  219. package/src/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.ts +96 -0
  220. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +255 -37
  221. package/src/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.ts +146 -0
  222. package/src/lib/hexclave-app/apps/implementations/common.ts +2 -1
  223. package/src/lib/hexclave-app/apps/implementations/redirect-page-urls.test.ts +1 -0
  224. package/src/lib/hexclave-app/teams/index.ts +3 -0
  225. package/src/pushed-config-error-overlay/index.ts +45 -386
  226. package/src/setup-error-overlay/index.test.ts +133 -0
  227. package/src/setup-error-overlay/index.ts +150 -0
  228. package/src/utils/monkey-patch.tsx +31 -0
  229. package/src/utils/react.tsx +93 -0
  230. package/dist/admin-app-impl-sTQD2d-3.d.ts.map +0 -1
  231. package/dist/client-app-BCT5p_70.d.ts.map +0 -1
@@ -9,6 +9,7 @@ import { runAsynchronously, runAsynchronouslyWithAlert } from "@hexclave/shared/
9
9
  import { isLocalhost } from "@hexclave/shared/dist/utils/urls";
10
10
  import type { StackClientApp } from "../lib/hexclave-app";
11
11
  import { getGlobalUiInstance, h, hasAppendChild, setGlobalUiInstance, setHtml, type UiGlobalInstance } from "../in-page-ui/dom";
12
+ import { HEXCLAVE_LOGO_SVG } from "../in-page-ui/logo";
12
13
  import { getBaseUrl } from "../lib/hexclave-app/apps/implementations/common";
13
14
  import type { HandlerUrlOptions, HandlerUrls, HandlerUrlTarget } from "../lib/hexclave-app/common";
14
15
  import { hexclaveAppInternalsSymbol } from "../lib/hexclave-app/common";
@@ -88,10 +89,6 @@ const DEFAULT_STATE: DevToolState = {
88
89
  panelHeight: 520,
89
90
  };
90
91
 
91
- // Hexclave mark — hexagon outline with three radial bars, monochrome via currentColor
92
- // so it inherits the trigger logo's color. Sourced from apps/dashboard/public/hexclave-icon.svg
93
- // (gradient + glow stripped; this is a tiny trigger glyph, not the full brand mark).
94
- 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>';
95
92
 
96
93
  // ---------------------------------------------------------------------------
97
94
  // State management
@@ -6,9 +6,8 @@
6
6
  import type { StackClientApp } from "../lib/hexclave-app";
7
7
  import { captureError } from "@hexclave/shared/dist/utils/errors";
8
8
  import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
9
- import { isLocalhost } from "@hexclave/shared/dist/utils/urls";
9
+ import { isLikelyDevelopmentEnvironment } from "../in-page-ui/dev-environment";
10
10
  import { canMountIntoDom } from "../in-page-ui/dom";
11
- import { envVars } from "../generated/env";
12
11
  import type { createDevTool as CreateDevToolFn } from "./dev-tool-core";
13
12
 
14
13
  // Hexclave rebrand: UI-only local pref — straight rename (one-time reset is harmless)
@@ -38,19 +37,9 @@ function shouldShow(): boolean {
38
37
  // consuming app itself was built or hosted as production.
39
38
  if (activeProjectIsDevelopmentEnvironment) return true;
40
39
 
41
- // "auto" behavior (the default):
42
- const nodeEnv = envVars.NODE_ENV;
43
- if (nodeEnv !== undefined) {
44
- // NODE_ENV is available (bundler/process env exists) — only show in development
45
- return nodeEnv === "development";
46
- }
47
-
48
- // NODE_ENV not found (no process.env/import.meta) — show on localhost or file: protocol
49
- try {
50
- const url = new URL(window.location.href);
51
- if (url.protocol === "file:") return true;
52
- } catch {}
53
- return isLocalhost(window.location.href);
40
+ // "auto" behavior (the default): every in-page UI that is only appropriate while developing shares one heuristic, so
41
+ // the dev tool and the error cards can never disagree about whether this looks like a development environment.
42
+ return isLikelyDevelopmentEnvironment();
54
43
  }
55
44
 
56
45
  let activeCleanup: (() => void) | null = null;
@@ -0,0 +1,33 @@
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
+ // Shared "are we in a development-like environment?" check for Hexclave's in-page UIs. Some of them (eg. surfacing an
6
+ // unexpected internal error) are only appropriate while developing, and the SDK cannot ask the server about it because
7
+ // the check has to work synchronously and before the app is authenticated.
8
+
9
+ import { isLocalhost } from "@hexclave/shared/dist/utils/urls";
10
+ import { envVars } from "../generated/env";
11
+
12
+ export function isLikelyDevelopmentEnvironment(): boolean {
13
+ // NODE_ENV is the most reliable signal, but plenty of bundlers (eg. Vite-based ones) don't define it in the browser
14
+ // bundle at all, so fall back to guessing from the current URL.
15
+ const nodeEnv = envVars.NODE_ENV;
16
+ if (nodeEnv !== undefined) {
17
+ return nodeEnv === "development";
18
+ }
19
+
20
+ if (typeof window === "undefined") {
21
+ return false;
22
+ }
23
+
24
+ try {
25
+ const url = new URL(window.location.href);
26
+ if (url.protocol === "file:") {
27
+ return true;
28
+ }
29
+ } catch {
30
+ return false;
31
+ }
32
+ return isLocalhost(window.location.href);
33
+ }
@@ -0,0 +1,67 @@
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 { afterEach, describe, expect, it } from "vitest";
8
+ import { renderIssueCard, trapFocusInIssueCard } from "./issue-card";
9
+
10
+ function mountCard(): HTMLElement {
11
+ const root = document.body.appendChild(document.createElement("div"));
12
+ root.appendChild(renderIssueCard({
13
+ kind: "error",
14
+ badge: "Config error",
15
+ title: "Something is misconfigured",
16
+ bodyText: "Body",
17
+ messageLabel: "Error message",
18
+ message: "Message",
19
+ footerText: "Footer",
20
+ ariaLabel: "Hexclave config error",
21
+ aiPrompt: "prompt",
22
+ onCopyError: () => {},
23
+ onCopyAiPromptError: () => {},
24
+ onMinimize: () => {},
25
+ }));
26
+ return root;
27
+ }
28
+
29
+ function pressTab() {
30
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true }));
31
+ }
32
+
33
+ function cardButtons(root: HTMLElement): HTMLButtonElement[] {
34
+ return [...root.querySelectorAll<HTMLButtonElement>(".hic-card button")];
35
+ }
36
+
37
+ describe("issue card focus trap", () => {
38
+ afterEach(() => {
39
+ document.body.replaceChildren();
40
+ });
41
+
42
+ it("follows a re-rendered card without moving focus or losing the original focus owner", () => {
43
+ const pageButton = document.body.appendChild(document.createElement("button"));
44
+ pageButton.focus();
45
+
46
+ const firstRoot = mountCard();
47
+ const firstTrap = trapFocusInIssueCard(firstRoot);
48
+ expect(document.activeElement).toBe(firstRoot.querySelector(".hic-card"));
49
+
50
+ // What the pushed-config overlay does on every poll: throw the rendered card away and render a fresh one.
51
+ firstRoot.remove();
52
+ const secondRoot = mountCard();
53
+ firstTrap.release({ restoreFocus: false });
54
+ const secondTrap = trapFocusInIssueCard(secondRoot, {
55
+ moveFocusIntoCard: false,
56
+ previouslyFocused: firstTrap.previouslyFocused,
57
+ });
58
+ expect(document.activeElement).not.toBe(secondRoot.querySelector(".hic-card"));
59
+
60
+ // The trap now belongs to the visible card, so Tab lands inside it instead of inside the detached one.
61
+ pressTab();
62
+ expect(document.activeElement).toBe(cardButtons(secondRoot)[0]);
63
+
64
+ secondTrap.release();
65
+ expect(document.activeElement).toBe(pageButton);
66
+ });
67
+ });
@@ -0,0 +1,500 @@
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
+ // Shared renderer for Hexclave's in-page issue cards: the modal-style card (plus its minimized pill) that the SDK uses
6
+ // to tell developers about problems it cannot recover from, like a broken pushed config or an untrusted auth domain.
7
+ // Every issue surface renders the same shape, so the markup, styles and clipboard handling live here and each feature
8
+ // only supplies its copy and callbacks.
9
+
10
+ import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
11
+ import { getInPageUiBaseCSS } from "./base-styles";
12
+ import { h, setHtml } from "./dom";
13
+ import { HEXCLAVE_LOGO_SVG } from "./logo";
14
+
15
+ export type IssueCardKind = "error" | "warning";
16
+
17
+ 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>';
18
+
19
+ export function getIssueCardCSS(scopeSelector: string): string {
20
+ return getInPageUiBaseCSS(scopeSelector) + `
21
+ ${scopeSelector} .hic-backdrop {
22
+ position: fixed;
23
+ inset: 0;
24
+ z-index: 2147483647;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ padding: 24px;
29
+ background: rgba(0, 0, 0, 0.46);
30
+ backdrop-filter: blur(6px);
31
+ overflow: auto;
32
+ }
33
+
34
+ ${scopeSelector} .hic-card {
35
+ --hic-status: var(--sdt-error);
36
+ width: min(720px, calc(100vw - 32px));
37
+ max-height: min(640px, calc(100dvh - 48px));
38
+ border: 1px solid color-mix(in srgb, var(--hic-status) 35%, var(--sdt-border));
39
+ border-radius: 18px;
40
+ background: var(--sdt-overlay-bg);
41
+ box-shadow: var(--sdt-shadow);
42
+ backdrop-filter: blur(18px);
43
+ display: flex;
44
+ overflow: hidden;
45
+ }
46
+
47
+ ${scopeSelector} .hic-card-warning {
48
+ --hic-status: var(--sdt-warning);
49
+ }
50
+
51
+ ${scopeSelector} .hic-card-inner {
52
+ padding: 18px;
53
+ width: 100%;
54
+ overflow: auto;
55
+ }
56
+
57
+ ${scopeSelector} .hic-header {
58
+ display: flex;
59
+ align-items: flex-start;
60
+ justify-content: space-between;
61
+ gap: 12px;
62
+ margin-bottom: 12px;
63
+ }
64
+
65
+ ${scopeSelector} .hic-title-row {
66
+ display: flex;
67
+ align-items: flex-start;
68
+ gap: 10px;
69
+ min-width: 0;
70
+ }
71
+
72
+ ${scopeSelector} .hic-logo {
73
+ flex-shrink: 0;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ width: 34px;
78
+ height: 34px;
79
+ border-radius: 10px;
80
+ background: var(--hic-status);
81
+ color: white;
82
+ box-shadow: 0 10px 30px color-mix(in srgb, var(--hic-status) 32%, transparent);
83
+ }
84
+
85
+ ${scopeSelector} .hic-badge {
86
+ display: inline-flex;
87
+ flex-shrink: 0;
88
+ padding: 2px 6px;
89
+ border-radius: 999px;
90
+ background: var(--hic-status);
91
+ color: white;
92
+ font-size: 10px;
93
+ font-weight: 700;
94
+ letter-spacing: 0.06em;
95
+ text-transform: uppercase;
96
+ }
97
+
98
+ ${scopeSelector} .hic-title {
99
+ color: var(--sdt-text);
100
+ margin-top: 4px;
101
+ font-size: 18px;
102
+ font-weight: 700;
103
+ line-height: 1.25;
104
+ }
105
+
106
+ ${scopeSelector} .hic-actions {
107
+ display: flex;
108
+ gap: 4px;
109
+ }
110
+
111
+ ${scopeSelector} .hic-icon-btn {
112
+ display: inline-flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ width: 28px;
116
+ height: 28px;
117
+ border: 1px solid var(--sdt-border);
118
+ border-radius: 8px;
119
+ background: var(--sdt-bg-elevated);
120
+ color: var(--sdt-text-secondary);
121
+ cursor: pointer;
122
+ font: inherit;
123
+ line-height: 1;
124
+ vertical-align: top;
125
+ }
126
+
127
+ ${scopeSelector} .hic-icon-btn svg {
128
+ display: block;
129
+ flex-shrink: 0;
130
+ }
131
+
132
+ ${scopeSelector} .hic-text-btn {
133
+ align-items: center;
134
+ gap: 6px;
135
+ min-height: 28px;
136
+ padding: 0 10px;
137
+ width: auto;
138
+ font-size: 12px;
139
+ line-height: 1;
140
+ }
141
+
142
+ ${scopeSelector} .hic-icon-btn:hover {
143
+ background: var(--sdt-bg-hover);
144
+ color: var(--sdt-text);
145
+ }
146
+
147
+ ${scopeSelector} .hic-body {
148
+ color: var(--sdt-text-secondary);
149
+ font-size: 14px;
150
+ line-height: 1.5;
151
+ }
152
+
153
+ ${scopeSelector} .hic-message-header {
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: space-between;
157
+ gap: 10px;
158
+ margin-top: 14px;
159
+ margin-bottom: 8px;
160
+ }
161
+
162
+ ${scopeSelector} .hic-message-label {
163
+ color: var(--sdt-text);
164
+ font-size: 12px;
165
+ font-weight: 650;
166
+ }
167
+
168
+ ${scopeSelector} .hic-message {
169
+ padding: 12px;
170
+ max-height: min(260px, max(96px, 30dvh));
171
+ overflow: auto;
172
+ border: 1px solid var(--sdt-border-subtle);
173
+ border-radius: 10px;
174
+ background: var(--sdt-bg-subtle);
175
+ color: var(--sdt-text);
176
+ font-family: var(--sdt-font-mono);
177
+ font-size: 12px;
178
+ white-space: pre-wrap;
179
+ overflow-wrap: anywhere;
180
+ }
181
+
182
+ ${scopeSelector} .hic-fix-list {
183
+ margin: 8px 0 0;
184
+ padding-left: 18px;
185
+ color: var(--sdt-text);
186
+ font-size: 13px;
187
+ }
188
+
189
+ ${scopeSelector} .hic-fix-list li + li {
190
+ margin-top: 4px;
191
+ }
192
+
193
+ ${scopeSelector} .hic-footer {
194
+ margin-top: 10px;
195
+ color: var(--sdt-text-tertiary);
196
+ font-size: 12px;
197
+ }
198
+
199
+ ${scopeSelector} .hic-pill {
200
+ position: fixed;
201
+ right: 18px;
202
+ bottom: 18px;
203
+ z-index: 2147483647;
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 8px;
207
+ padding: 8px 12px 8px 8px;
208
+ --hic-status: var(--sdt-error);
209
+ border: 1px solid color-mix(in srgb, var(--hic-status) 35%, var(--sdt-border));
210
+ border-radius: 999px;
211
+ background: var(--sdt-overlay-bg);
212
+ box-shadow: var(--sdt-trigger-shadow);
213
+ color: var(--sdt-text);
214
+ cursor: pointer;
215
+ font: inherit;
216
+ backdrop-filter: blur(18px);
217
+ }
218
+
219
+ ${scopeSelector} .hic-pill-warning {
220
+ --hic-status: var(--sdt-warning);
221
+ }
222
+
223
+ ${scopeSelector} .hic-pill-logo {
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ width: 26px;
228
+ height: 26px;
229
+ border-radius: 999px;
230
+ background: var(--hic-status);
231
+ color: white;
232
+ }
233
+
234
+ @media (max-height: 520px) {
235
+ ${scopeSelector} .hic-backdrop {
236
+ align-items: flex-start;
237
+ padding: 12px;
238
+ }
239
+
240
+ ${scopeSelector} .hic-card {
241
+ width: min(720px, calc(100vw - 24px));
242
+ max-height: calc(100dvh - 24px);
243
+ }
244
+
245
+ ${scopeSelector} .hic-card-inner {
246
+ padding: 12px;
247
+ }
248
+
249
+ ${scopeSelector} .hic-header {
250
+ margin-bottom: 8px;
251
+ }
252
+
253
+ ${scopeSelector} .hic-title {
254
+ font-size: 16px;
255
+ }
256
+
257
+ ${scopeSelector} .hic-body {
258
+ font-size: 13px;
259
+ }
260
+
261
+ ${scopeSelector} .hic-message {
262
+ max-height: max(80px, 24dvh);
263
+ }
264
+ }
265
+ `;
266
+ }
267
+
268
+ export async function copyTextToClipboard(text: string): Promise<void> {
269
+ const clipboard: unknown = Reflect.get(navigator, "clipboard");
270
+ const writeText = clipboard != null && typeof clipboard === "object"
271
+ ? Reflect.get(clipboard, "writeText")
272
+ : null;
273
+ if (typeof writeText === "function") {
274
+ await writeText.call(clipboard, text);
275
+ return;
276
+ }
277
+
278
+ const textarea = h("textarea", {
279
+ style: {
280
+ position: "fixed",
281
+ left: "-9999px",
282
+ top: "0",
283
+ opacity: "0",
284
+ },
285
+ readonly: "true",
286
+ });
287
+ textarea.value = text;
288
+ document.body.appendChild(textarea);
289
+ textarea.select();
290
+ const copied = document.execCommand("copy");
291
+ textarea.remove();
292
+ if (!copied) {
293
+ throw new Error("Browser refused to copy the message.");
294
+ }
295
+ }
296
+
297
+ /**
298
+ * A button that copies `getText()` and briefly reports the outcome in its own label. Copying can fail for reasons
299
+ * outside our control (permissions, insecure contexts), so the failure is reported inline instead of being thrown into
300
+ * a click handler where nothing would show it.
301
+ */
302
+ function renderCopyButton(options: {
303
+ variant: "icon" | "text",
304
+ title: string,
305
+ ariaLabel: string,
306
+ getText: () => string,
307
+ onCopyError: (error: unknown) => void,
308
+ }): HTMLElement {
309
+ const idleHtml = options.variant === "text" ? `${COPY_ICON_SVG}Copy` : COPY_ICON_SVG;
310
+ const button = h("button", {
311
+ className: options.variant === "text" ? "hic-icon-btn hic-text-btn" : "hic-icon-btn",
312
+ type: "button",
313
+ title: options.title,
314
+ "aria-label": options.ariaLabel,
315
+ onClick: () => {
316
+ runAsynchronously(async () => {
317
+ await copyTextToClipboard(options.getText());
318
+ button.textContent = options.variant === "text" ? "Copied" : "✓";
319
+ setTimeout(() => setHtml(button, idleHtml), 1500);
320
+ }, {
321
+ noErrorLogging: true,
322
+ onError: (error) => {
323
+ options.onCopyError(error);
324
+ button.textContent = options.variant === "text" ? "Copy failed" : "!";
325
+ setTimeout(() => setHtml(button, idleHtml), 1500);
326
+ },
327
+ });
328
+ },
329
+ });
330
+ setHtml(button, idleHtml);
331
+ return button;
332
+ }
333
+
334
+ export type IssueCardOptions = {
335
+ kind: IssueCardKind,
336
+ /** Short uppercase label in the card's badge, eg. "Config error". */
337
+ badge: string,
338
+ title: string,
339
+ /** One or two sentences on what this means for the running app. */
340
+ bodyText: string,
341
+ messageLabel: string,
342
+ message: string,
343
+ /** Concrete steps the developer should take; rendered as a list above the footer. */
344
+ howToFix?: readonly string[],
345
+ footerText: string,
346
+ ariaLabel: string,
347
+ /** Prompt for the "copy AI prompt" button, ie. the message plus enough context for a coding agent to fix it. */
348
+ aiPrompt: string,
349
+ onCopyError: (error: unknown) => void,
350
+ onCopyAiPromptError: (error: unknown) => void,
351
+ onMinimize: () => void,
352
+ };
353
+
354
+ export function renderIssueCard(options: IssueCardOptions): HTMLElement {
355
+ const logoSpan = h("span", { className: "hic-logo" });
356
+ setHtml(logoSpan, HEXCLAVE_LOGO_SVG);
357
+
358
+ return h("div", { className: "hic-backdrop" },
359
+ h("div", { className: options.kind === "error" ? "hic-card" : "hic-card hic-card-warning", role: "alertdialog", "aria-modal": "true", "aria-label": options.ariaLabel, tabindex: "-1" },
360
+ h("div", { className: "hic-card-inner" },
361
+ h("div", { className: "hic-header" },
362
+ h("div", { className: "hic-title-row" },
363
+ logoSpan,
364
+ h("div", null,
365
+ h("span", { className: "hic-badge" }, options.badge),
366
+ h("div", { className: "hic-title" }, options.title),
367
+ ),
368
+ ),
369
+ h("div", { className: "hic-actions" },
370
+ renderCopyButton({
371
+ variant: "icon",
372
+ title: "Copy AI prompt",
373
+ ariaLabel: `Copy AI prompt for ${options.ariaLabel}`,
374
+ getText: () => options.aiPrompt,
375
+ onCopyError: options.onCopyAiPromptError,
376
+ }),
377
+ h("button", {
378
+ className: "hic-icon-btn",
379
+ type: "button",
380
+ title: "Minimize",
381
+ "aria-label": `Minimize ${options.ariaLabel}`,
382
+ onClick: options.onMinimize,
383
+ }, "–"),
384
+ ),
385
+ ),
386
+ h("div", { className: "hic-body" },
387
+ options.bodyText,
388
+ h("div", { className: "hic-message-header" },
389
+ h("div", { className: "hic-message-label" }, options.messageLabel),
390
+ renderCopyButton({
391
+ variant: "text",
392
+ title: `Copy ${options.messageLabel.toLowerCase()}`,
393
+ ariaLabel: `Copy ${options.ariaLabel} message`,
394
+ getText: () => options.message,
395
+ onCopyError: options.onCopyError,
396
+ }),
397
+ ),
398
+ h("div", { className: "hic-message" }, options.message),
399
+ options.howToFix == null || options.howToFix.length === 0 ? null : h("div", null,
400
+ h("div", { className: "hic-message-header" },
401
+ h("div", { className: "hic-message-label" }, "How to fix"),
402
+ ),
403
+ h("ul",
404
+ { className: "hic-fix-list" },
405
+ ...options.howToFix.map((step) => h("li", null, step)),
406
+ ),
407
+ ),
408
+ h("div", { className: "hic-footer" }, options.footerText),
409
+ ),
410
+ ),
411
+ ),
412
+ );
413
+ }
414
+
415
+ const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])";
416
+
417
+ export type IssueCardFocusTrap = {
418
+ /**
419
+ * Whatever had focus before the card first appeared. Callers that rebind the trap to a re-rendered card pass it back
420
+ * in, so focus still returns to where the developer left it once the card is gone for good.
421
+ */
422
+ previouslyFocused: HTMLElement | null,
423
+ release: (releaseOptions?: { restoreFocus?: boolean }) => void,
424
+ };
425
+
426
+ /**
427
+ * Keeps keyboard focus inside a mounted card, which claims to be an `aria-modal` alertdialog and therefore has to behave
428
+ * like one: the flow behind the card is dead until the problem is fixed, so tabbing into it would only let a keyboard
429
+ * user wander through a page that cannot work while the card stays invisible to them.
430
+ *
431
+ * Call right after mounting the card, and release the trap when the card is minimized, replaced or removed. Releasing
432
+ * hands focus back to `previouslyFocused` unless `restoreFocus: false` is passed, which is what a caller that is about
433
+ * to rebind the trap to a freshly rendered card wants.
434
+ */
435
+ export function trapFocusInIssueCard(cardRoot: HTMLElement, options: {
436
+ moveFocusIntoCard?: boolean,
437
+ previouslyFocused?: HTMLElement | null,
438
+ } = {}): IssueCardFocusTrap {
439
+ const previouslyFocused = options.previouslyFocused !== undefined
440
+ ? options.previouslyFocused
441
+ : document.activeElement instanceof HTMLElement ? document.activeElement : null;
442
+ const card = cardRoot.querySelector(".hic-card");
443
+ if (!(card instanceof HTMLElement)) return { previouslyFocused, release: () => {} };
444
+
445
+ const onKeyDown = (event: KeyboardEvent) => {
446
+ if (event.key !== "Tab") return;
447
+ const focusable = [...card.querySelectorAll(FOCUSABLE_SELECTOR)].filter((element): element is HTMLElement => element instanceof HTMLElement);
448
+ const first = focusable[0] ?? card;
449
+ const last = focusable[focusable.length - 1] ?? card;
450
+ const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
451
+
452
+ let target: HTMLElement | null = null;
453
+ if (active == null || !card.contains(active)) {
454
+ // Focus is outside the card (the page moved it, or it never got in): pull it back to the card's near end.
455
+ target = event.shiftKey ? last : first;
456
+ } else if (active === last && !event.shiftKey) {
457
+ target = first;
458
+ } else if (active === first && event.shiftKey) {
459
+ target = last;
460
+ }
461
+ if (target == null) return;
462
+ event.preventDefault();
463
+ target.focus();
464
+ };
465
+
466
+ document.addEventListener("keydown", onKeyDown, true);
467
+ if (options.moveFocusIntoCard !== false) {
468
+ card.focus();
469
+ }
470
+
471
+ return {
472
+ previouslyFocused,
473
+ release: (releaseOptions) => {
474
+ document.removeEventListener("keydown", onKeyDown, true);
475
+ if (releaseOptions?.restoreFocus === false) return;
476
+ if (previouslyFocused != null && previouslyFocused.isConnected) {
477
+ previouslyFocused.focus();
478
+ }
479
+ },
480
+ };
481
+ }
482
+
483
+ export function renderIssuePill(options: {
484
+ kind: IssueCardKind,
485
+ label: string,
486
+ ariaLabel: string,
487
+ onClick: () => void,
488
+ }): HTMLElement {
489
+ const logoSpan = h("span", { className: "hic-pill-logo" });
490
+ setHtml(logoSpan, HEXCLAVE_LOGO_SVG);
491
+ return h("button", {
492
+ className: options.kind === "error" ? "hic-pill" : "hic-pill hic-pill-warning",
493
+ type: "button",
494
+ "aria-label": options.ariaLabel,
495
+ onClick: options.onClick,
496
+ },
497
+ logoSpan,
498
+ h("span", null, options.label),
499
+ );
500
+ }
@@ -0,0 +1,8 @@
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
+ // Hexclave mark — hexagon outline with three radial bars, monochrome via currentColor so it inherits the surrounding
6
+ // text color. Sourced from apps/dashboard/public/hexclave-icon.svg (gradient + glow stripped; this is a tiny glyph, not
7
+ // the full brand mark). Shared by every in-page UI so the mark cannot drift between them.
8
+ export 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>';