@hexclave/next 1.0.13 → 1.0.15

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 (189) hide show
  1. package/dist/clickmap/clickmap-core.d.ts +15 -0
  2. package/dist/clickmap/clickmap-core.d.ts.map +1 -0
  3. package/dist/clickmap/clickmap-core.js +1527 -0
  4. package/dist/clickmap/clickmap-core.js.map +1 -0
  5. package/dist/clickmap/clickmap-styles.d.ts +5 -0
  6. package/dist/clickmap/clickmap-styles.d.ts.map +1 -0
  7. package/dist/clickmap/clickmap-styles.js +1095 -0
  8. package/dist/clickmap/clickmap-styles.js.map +1 -0
  9. package/dist/clickmap/index.d.ts +16 -0
  10. package/dist/clickmap/index.d.ts.map +1 -0
  11. package/dist/clickmap/index.js +74 -0
  12. package/dist/clickmap/index.js.map +1 -0
  13. package/dist/components/api-key-dialogs.js +2 -2
  14. package/dist/components/credential-sign-in.js +1 -1
  15. package/dist/components/credential-sign-up.js +1 -1
  16. package/dist/components/magic-link-sign-in.js +1 -1
  17. package/dist/components/message-cards/known-error-message-card.d.ts +1 -1
  18. package/dist/components/team-switcher.js +1 -1
  19. package/dist/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
  20. package/dist/components-page/account-settings/email-and-auth/emails-section.js +1 -1
  21. package/dist/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
  22. package/dist/components-page/account-settings/email-and-auth/password-section.js +1 -1
  23. package/dist/components-page/account-settings/teams/team-creation-page.js +1 -1
  24. package/dist/components-page/account-settings/teams/team-member-invitation-section.js +1 -1
  25. package/dist/components-page/auth-page.js +1 -1
  26. package/dist/components-page/cli-auth-confirm.js +1 -1
  27. package/dist/components-page/cli-auth-confirm.test.js +1 -1
  28. package/dist/components-page/forgot-password.d.ts.map +1 -1
  29. package/dist/components-page/forgot-password.js +2 -3
  30. package/dist/components-page/forgot-password.js.map +1 -1
  31. package/dist/components-page/hexclave-handler-client.d.ts +1 -1
  32. package/dist/components-page/mfa.js +4 -19
  33. package/dist/components-page/mfa.js.map +1 -1
  34. package/dist/components-page/oauth-callback.js +1 -1
  35. package/dist/components-page/onboarding.js +1 -1
  36. package/dist/components-page/password-reset.d.ts.map +1 -1
  37. package/dist/components-page/password-reset.js +5 -7
  38. package/dist/components-page/password-reset.js.map +1 -1
  39. package/dist/components-page/team-creation.js +1 -1
  40. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  41. package/dist/dev-tool/dev-tool-core.js +258 -262
  42. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  43. package/dist/dev-tool/dev-tool-styles.d.ts +1 -1
  44. package/dist/dev-tool/dev-tool-styles.d.ts.map +1 -1
  45. package/dist/dev-tool/dev-tool-styles.js +13 -143
  46. package/dist/dev-tool/dev-tool-styles.js.map +1 -1
  47. package/dist/dev-tool/index.d.ts.map +1 -1
  48. package/dist/dev-tool/index.js +5 -12
  49. package/dist/dev-tool/index.js.map +1 -1
  50. package/dist/esm/clickmap/clickmap-core.d.ts +15 -0
  51. package/dist/esm/clickmap/clickmap-core.d.ts.map +1 -0
  52. package/dist/esm/clickmap/clickmap-core.js +1525 -0
  53. package/dist/esm/clickmap/clickmap-core.js.map +1 -0
  54. package/dist/esm/clickmap/clickmap-styles.d.ts +5 -0
  55. package/dist/esm/clickmap/clickmap-styles.d.ts.map +1 -0
  56. package/dist/esm/clickmap/clickmap-styles.js +1093 -0
  57. package/dist/esm/clickmap/clickmap-styles.js.map +1 -0
  58. package/dist/esm/clickmap/index.d.ts +16 -0
  59. package/dist/esm/clickmap/index.d.ts.map +1 -0
  60. package/dist/esm/clickmap/index.js +72 -0
  61. package/dist/esm/clickmap/index.js.map +1 -0
  62. package/dist/esm/components/api-key-dialogs.js +2 -2
  63. package/dist/esm/components/credential-sign-in.js +1 -1
  64. package/dist/esm/components/credential-sign-up.js +1 -1
  65. package/dist/esm/components/magic-link-sign-in.js +1 -1
  66. package/dist/esm/components/team-switcher.js +1 -1
  67. package/dist/esm/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
  68. package/dist/esm/components-page/account-settings/email-and-auth/emails-section.js +1 -1
  69. package/dist/esm/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
  70. package/dist/esm/components-page/account-settings/email-and-auth/password-section.js +1 -1
  71. package/dist/esm/components-page/account-settings/teams/team-creation-page.js +1 -1
  72. package/dist/esm/components-page/account-settings/teams/team-member-invitation-section.js +1 -1
  73. package/dist/esm/components-page/auth-page.js +1 -1
  74. package/dist/esm/components-page/cli-auth-confirm.js +1 -1
  75. package/dist/esm/components-page/cli-auth-confirm.test.js +1 -1
  76. package/dist/esm/components-page/forgot-password.d.ts.map +1 -1
  77. package/dist/esm/components-page/forgot-password.js +2 -3
  78. package/dist/esm/components-page/forgot-password.js.map +1 -1
  79. package/dist/esm/components-page/hexclave-handler-client.d.ts +1 -1
  80. package/dist/esm/components-page/mfa.js +4 -19
  81. package/dist/esm/components-page/mfa.js.map +1 -1
  82. package/dist/esm/components-page/oauth-callback.js +1 -1
  83. package/dist/esm/components-page/onboarding.js +1 -1
  84. package/dist/esm/components-page/password-reset.d.ts.map +1 -1
  85. package/dist/esm/components-page/password-reset.js +5 -7
  86. package/dist/esm/components-page/password-reset.js.map +1 -1
  87. package/dist/esm/components-page/team-creation.js +1 -1
  88. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  89. package/dist/esm/dev-tool/dev-tool-core.js +35 -39
  90. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  91. package/dist/esm/dev-tool/dev-tool-styles.d.ts +1 -1
  92. package/dist/esm/dev-tool/dev-tool-styles.d.ts.map +1 -1
  93. package/dist/esm/dev-tool/dev-tool-styles.js +13 -143
  94. package/dist/esm/dev-tool/dev-tool-styles.js.map +1 -1
  95. package/dist/esm/dev-tool/index.d.ts.map +1 -1
  96. package/dist/esm/dev-tool/index.js +2 -9
  97. package/dist/esm/dev-tool/index.js.map +1 -1
  98. package/dist/esm/generated/global-css.d.ts +1 -1
  99. package/dist/esm/generated/global-css.js +1 -1
  100. package/dist/esm/generated/global-css.js.map +1 -1
  101. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  102. package/dist/esm/in-page-ui/base-styles.d.ts +5 -0
  103. package/dist/esm/in-page-ui/base-styles.d.ts.map +1 -0
  104. package/dist/esm/in-page-ui/base-styles.js +166 -0
  105. package/dist/esm/in-page-ui/base-styles.js.map +1 -0
  106. package/dist/esm/in-page-ui/dom.d.ts +15 -0
  107. package/dist/esm/in-page-ui/dom.d.ts.map +1 -0
  108. package/dist/esm/in-page-ui/dom.js +44 -0
  109. package/dist/esm/in-page-ui/dom.js.map +1 -0
  110. package/dist/esm/lib/auth.js +1 -1
  111. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -1
  112. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  113. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +20 -0
  114. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  115. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  116. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -2
  117. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  118. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
  119. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts +13 -0
  120. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  121. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +146 -14
  122. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  123. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +221 -0
  124. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  125. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  126. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
  127. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +1 -1
  128. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +5 -0
  129. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  130. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  131. package/dist/esm/providers/theme-provider.js +1 -1
  132. package/dist/generated/global-css.d.ts +1 -1
  133. package/dist/generated/global-css.js +1 -1
  134. package/dist/generated/global-css.js.map +1 -1
  135. package/dist/generated/quetzal-translations.d.ts +2 -2
  136. package/dist/in-page-ui/base-styles.d.ts +5 -0
  137. package/dist/in-page-ui/base-styles.d.ts.map +1 -0
  138. package/dist/in-page-ui/base-styles.js +168 -0
  139. package/dist/in-page-ui/base-styles.js.map +1 -0
  140. package/dist/in-page-ui/dom.d.ts +15 -0
  141. package/dist/in-page-ui/dom.d.ts.map +1 -0
  142. package/dist/in-page-ui/dom.js +51 -0
  143. package/dist/in-page-ui/dom.js.map +1 -0
  144. package/dist/index.d.ts +1 -1
  145. package/dist/integrations/convex/component/convex.config.d.ts +1 -1
  146. package/dist/lib/auth.js +1 -1
  147. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -1
  148. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  149. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +20 -0
  150. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  151. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  152. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -2
  153. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  154. package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
  155. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +13 -0
  156. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  157. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +146 -14
  158. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  159. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +221 -0
  160. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  161. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  162. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
  163. package/dist/lib/hexclave-app/apps/implementations/session-replay.js +1 -1
  164. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +5 -0
  165. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  166. package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  167. package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
  168. package/dist/lib/hexclave-app/common.d.ts +1 -1
  169. package/dist/providers/hexclave-provider-client.d.ts +1 -1
  170. package/dist/providers/theme-provider.js +1 -1
  171. package/dist/{storage-CKzvsBxG.d.ts → storage-ksajV_p6.d.ts} +1 -1
  172. package/dist/{storage-CKzvsBxG.d.ts.map → storage-ksajV_p6.d.ts.map} +1 -1
  173. package/package.json +4 -4
  174. package/src/clickmap/clickmap-core.ts +1997 -0
  175. package/src/clickmap/clickmap-styles.ts +1102 -0
  176. package/src/clickmap/index.ts +95 -0
  177. package/src/components-page/forgot-password.tsx +1 -2
  178. package/src/components-page/mfa.tsx +12 -21
  179. package/src/components-page/password-reset.tsx +4 -6
  180. package/src/dev-tool/dev-tool-core.ts +38 -65
  181. package/src/dev-tool/dev-tool-styles.ts +13 -142
  182. package/src/dev-tool/index.ts +1 -14
  183. package/src/in-page-ui/base-styles.ts +171 -0
  184. package/src/in-page-ui/dom.ts +80 -0
  185. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +23 -1
  186. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +7 -0
  187. package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +287 -0
  188. package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +226 -16
  189. package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +3 -0
@@ -0,0 +1,171 @@
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 design tokens + base reset for Hexclave's in-page UIs (the dev tool
6
+ // and the standalone clickmap overlay). Each feature passes its own scope
7
+ // selector so the two stylesheets never collide, while the tokens stay defined
8
+ // in exactly one place. This module deliberately lives outside both feature
9
+ // folders so either feature can be removed without affecting the other.
10
+
11
+ export function getInPageUiBaseCSS(scopeSelector: string): string {
12
+ return `
13
+ ${scopeSelector} {
14
+ --sdt-bg: #0a0a0b;
15
+ --sdt-bg-elevated: #141416;
16
+ --sdt-bg-hover: #1c1c1f;
17
+ --sdt-bg-active: #232326;
18
+ --sdt-bg-subtle: #111113;
19
+ --sdt-border: #2a2a2e;
20
+ --sdt-border-subtle: #1e1e22;
21
+ --sdt-text: #ececef;
22
+ --sdt-text-secondary: #8b8b93;
23
+ --sdt-text-tertiary: #5c5c66;
24
+ --sdt-accent: #6366f1;
25
+ --sdt-accent-hover: #818cf8;
26
+ --sdt-accent-muted: rgba(99, 102, 241, 0.15);
27
+ --sdt-success: #22c55e;
28
+ --sdt-success-muted: rgba(34, 197, 94, 0.15);
29
+ --sdt-warning: #eab308;
30
+ --sdt-warning-muted: rgba(234, 179, 8, 0.15);
31
+ --sdt-error: #ef4444;
32
+ --sdt-error-muted: rgba(239, 68, 68, 0.15);
33
+ --sdt-info: #3b82f6;
34
+ --sdt-info-muted: rgba(59, 130, 246, 0.15);
35
+ --sdt-overlay-bg: rgba(17, 17, 19, 0.92);
36
+ --sdt-radius: 8px;
37
+ --sdt-radius-sm: 4px;
38
+ --sdt-radius-lg: 12px;
39
+ --sdt-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
40
+ --sdt-font-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace;
41
+ --sdt-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
42
+ --sdt-trigger-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
43
+
44
+ all: initial;
45
+ font-family: var(--sdt-font);
46
+ color: var(--sdt-text);
47
+ font-size: 13px;
48
+ line-height: 1.5;
49
+ -webkit-font-smoothing: antialiased;
50
+ -moz-osx-font-smoothing: grayscale;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ ${scopeSelector} *, ${scopeSelector} *::before, ${scopeSelector} *::after {
55
+ box-sizing: border-box;
56
+ }
57
+
58
+ /* Thin, unobtrusive scrollbars for every scroll container */
59
+ ${scopeSelector} * {
60
+ scrollbar-width: thin;
61
+ scrollbar-color: var(--sdt-border) transparent;
62
+ }
63
+
64
+ ${scopeSelector} *::-webkit-scrollbar {
65
+ width: 6px;
66
+ height: 6px;
67
+ }
68
+
69
+ ${scopeSelector} *::-webkit-scrollbar-track {
70
+ background: transparent;
71
+ }
72
+
73
+ ${scopeSelector} *::-webkit-scrollbar-thumb {
74
+ background: var(--sdt-border);
75
+ border-radius: 3px;
76
+ }
77
+
78
+ ${scopeSelector} *::-webkit-scrollbar-thumb:hover {
79
+ background: var(--sdt-text-tertiary);
80
+ }
81
+
82
+ ${scopeSelector} *::-webkit-scrollbar-corner {
83
+ background: transparent;
84
+ }
85
+
86
+ /* --- Light theme: system preference fallback --- */
87
+ @media (prefers-color-scheme: light) {
88
+ ${scopeSelector} {
89
+ --sdt-bg: #ffffff;
90
+ --sdt-bg-elevated: #f8f8fa;
91
+ --sdt-bg-hover: #f0f0f3;
92
+ --sdt-bg-active: #e8e8ec;
93
+ --sdt-bg-subtle: #fafafa;
94
+ --sdt-border: #e0e0e5;
95
+ --sdt-border-subtle: #eaeaef;
96
+ --sdt-text: #111113;
97
+ --sdt-text-secondary: #6b6b73;
98
+ --sdt-text-tertiary: #9b9ba3;
99
+ --sdt-accent: #6366f1;
100
+ --sdt-accent-hover: #4f46e5;
101
+ --sdt-accent-muted: rgba(99, 102, 241, 0.1);
102
+ --sdt-success: #16a34a;
103
+ --sdt-success-muted: rgba(22, 163, 74, 0.1);
104
+ --sdt-warning: #ca8a04;
105
+ --sdt-warning-muted: rgba(202, 138, 4, 0.1);
106
+ --sdt-error: #dc2626;
107
+ --sdt-error-muted: rgba(220, 38, 38, 0.1);
108
+ --sdt-info: #2563eb;
109
+ --sdt-info-muted: rgba(37, 99, 235, 0.1);
110
+ --sdt-overlay-bg: rgba(255, 255, 255, 0.92);
111
+ --sdt-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.06);
112
+ --sdt-trigger-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.06);
113
+ }
114
+ }
115
+
116
+ /* --- Stack theme explicit overrides (take priority over system preference) --- */
117
+ html:has(head > [data-stack-theme="light"]) ${scopeSelector} {
118
+ --sdt-bg: #ffffff;
119
+ --sdt-bg-elevated: #f8f8fa;
120
+ --sdt-bg-hover: #f0f0f3;
121
+ --sdt-bg-active: #e8e8ec;
122
+ --sdt-bg-subtle: #fafafa;
123
+ --sdt-border: #e0e0e5;
124
+ --sdt-border-subtle: #eaeaef;
125
+ --sdt-text: #111113;
126
+ --sdt-text-secondary: #6b6b73;
127
+ --sdt-text-tertiary: #9b9ba3;
128
+ --sdt-accent: #6366f1;
129
+ --sdt-accent-hover: #4f46e5;
130
+ --sdt-accent-muted: rgba(99, 102, 241, 0.1);
131
+ --sdt-success: #16a34a;
132
+ --sdt-success-muted: rgba(22, 163, 74, 0.1);
133
+ --sdt-warning: #ca8a04;
134
+ --sdt-warning-muted: rgba(202, 138, 4, 0.1);
135
+ --sdt-error: #dc2626;
136
+ --sdt-error-muted: rgba(220, 38, 38, 0.1);
137
+ --sdt-info: #2563eb;
138
+ --sdt-info-muted: rgba(37, 99, 235, 0.1);
139
+ --sdt-overlay-bg: rgba(255, 255, 255, 0.92);
140
+ --sdt-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.06);
141
+ --sdt-trigger-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.06);
142
+ }
143
+
144
+ html:has(head > [data-stack-theme="dark"]) ${scopeSelector} {
145
+ --sdt-bg: #0a0a0b;
146
+ --sdt-bg-elevated: #141416;
147
+ --sdt-bg-hover: #1c1c1f;
148
+ --sdt-bg-active: #232326;
149
+ --sdt-bg-subtle: #111113;
150
+ --sdt-border: #2a2a2e;
151
+ --sdt-border-subtle: #1e1e22;
152
+ --sdt-text: #ececef;
153
+ --sdt-text-secondary: #8b8b93;
154
+ --sdt-text-tertiary: #5c5c66;
155
+ --sdt-accent: #6366f1;
156
+ --sdt-accent-hover: #818cf8;
157
+ --sdt-accent-muted: rgba(99, 102, 241, 0.15);
158
+ --sdt-success: #22c55e;
159
+ --sdt-success-muted: rgba(34, 197, 94, 0.15);
160
+ --sdt-warning: #eab308;
161
+ --sdt-warning-muted: rgba(234, 179, 8, 0.15);
162
+ --sdt-error: #ef4444;
163
+ --sdt-error-muted: rgba(239, 68, 68, 0.15);
164
+ --sdt-info: #3b82f6;
165
+ --sdt-info-muted: rgba(59, 130, 246, 0.15);
166
+ --sdt-overlay-bg: rgba(17, 17, 19, 0.92);
167
+ --sdt-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
168
+ --sdt-trigger-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
169
+ }
170
+ `;
171
+ }
@@ -0,0 +1,80 @@
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
+ // Tiny DOM helpers shared by Hexclave's in-page UIs (the dev tool and the
6
+ // standalone clickmap overlay). This module deliberately lives outside both
7
+ // feature folders so either feature can be removed without affecting the other.
8
+
9
+ export function h<K extends keyof HTMLElementTagNameMap>(
10
+ tag: K,
11
+ attrs?: Record<string, any> | null,
12
+ ...children: (string | Node | null | undefined)[]
13
+ ): HTMLElementTagNameMap[K] {
14
+ const el = document.createElement(tag);
15
+ if (attrs) {
16
+ for (const [k, v] of Object.entries(attrs)) {
17
+ if (v == null) continue;
18
+ if (k === 'className') {
19
+ el.className = v;
20
+ } else if (k === 'style' && typeof v === 'object') {
21
+ Object.assign(el.style, v);
22
+ } else if (k.startsWith('on') && typeof v === 'function') {
23
+ el.addEventListener(k.slice(2).toLowerCase(), v);
24
+ } else {
25
+ el.setAttribute(k, String(v));
26
+ }
27
+ }
28
+ }
29
+ for (const child of children) {
30
+ if (child == null) continue;
31
+ el.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
32
+ }
33
+ return el;
34
+ }
35
+
36
+ export function setHtml(el: HTMLElement, html: string) {
37
+ el.innerHTML = html;
38
+ }
39
+
40
+ export function hasAppendChild(value: unknown): value is { appendChild(node: Node): void } {
41
+ return typeof value === 'object' && value !== null && typeof Reflect.get(value, 'appendChild') === 'function';
42
+ }
43
+
44
+ export function canMountIntoDom(): boolean {
45
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
46
+ return false;
47
+ }
48
+ if (typeof document.createElement !== 'function') {
49
+ return false;
50
+ }
51
+ return hasAppendChild(Reflect.get(document, 'body'));
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Window-global singleton slot, so remounts (e.g. across HMR or multiple app
56
+ // instances) can tear down the previous instance before mounting a new one.
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export type UiGlobalInstance = {
60
+ cleanup: () => void;
61
+ };
62
+
63
+ function isUiGlobalInstance(value: unknown): value is UiGlobalInstance {
64
+ return typeof value === 'object' && value !== null && typeof Reflect.get(value, 'cleanup') === 'function';
65
+ }
66
+
67
+ export function getGlobalUiInstance(key: string): UiGlobalInstance | null {
68
+ if (typeof window === 'undefined') return null;
69
+ const value: unknown = Reflect.get(window, key);
70
+ return isUiGlobalInstance(value) ? value : null;
71
+ }
72
+
73
+ export function setGlobalUiInstance(key: string, instance: UiGlobalInstance | null) {
74
+ if (typeof window === 'undefined') return;
75
+ if (instance === null) {
76
+ Reflect.deleteProperty(window, key);
77
+ } else {
78
+ Reflect.set(window, key, instance);
79
+ }
80
+ }
@@ -5,7 +5,7 @@
5
5
  import { KnownErrors, HexclaveAdminInterface } from "@hexclave/shared";
6
6
  import { getProductionModeErrors } from "@hexclave/shared/dist/helpers/production-mode";
7
7
  import { InternalApiKeyCreateCrudResponse } from "@hexclave/shared/dist/interface/admin-interface";
8
- import type { MetricsResponse, MetricsUserCounts, UserActivityResponse } from "@hexclave/shared/dist/interface/admin-metrics";
8
+ import type { AnalyticsClickmapOptions, AnalyticsClickmapResponse, AnalyticsClickmapTokenResponse, MetricsResponse, MetricsUserCounts, UserActivityResponse } from "@hexclave/shared/dist/interface/admin-metrics";
9
9
  import { AnalyticsQueryOptions, AnalyticsQueryResponse } from "@hexclave/shared/dist/interface/crud/analytics";
10
10
  import { EmailTemplateCrud } from "@hexclave/shared/dist/interface/crud/email-templates";
11
11
  import { InternalApiKeysCrud } from "@hexclave/shared/dist/interface/crud/internal-api-keys";
@@ -1151,6 +1151,28 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
1151
1151
  return await this._interface.queryAnalytics(options);
1152
1152
  }
1153
1153
 
1154
+ async getAnalyticsClickmap(options: AnalyticsClickmapOptions): Promise<AnalyticsClickmapResponse> {
1155
+ return await this._interface.getAnalyticsClickmap({
1156
+ kind: options.kind,
1157
+ member_user_ids: options.memberUserIds,
1158
+ route_path: options.routePath,
1159
+ route_regex: options.routeRegex,
1160
+ url_pattern: options.urlPattern,
1161
+ user_id: options.userId,
1162
+ replay_id: options.replayId,
1163
+ device: options.device,
1164
+ viewport_width_min: options.viewportWidthMin,
1165
+ viewport_width_max: options.viewportWidthMax,
1166
+ sampling: options.sampling,
1167
+ since: options.since,
1168
+ until: options.until,
1169
+ });
1170
+ }
1171
+
1172
+ async createAnalyticsClickmapToken(options: { origin: string }): Promise<AnalyticsClickmapTokenResponse> {
1173
+ return await this._interface.createAnalyticsClickmapToken(options);
1174
+ }
1175
+
1154
1176
  async listSessionReplays(options?: ListSessionReplaysOptions): Promise<ListSessionReplaysResult> {
1155
1177
  const response = await this._interface.listSessionReplays({
1156
1178
  cursor: options?.cursor,
@@ -70,6 +70,7 @@ import { subscribeSessionRefresh } from "./session-refresh-subscription";
70
70
  import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson } from "./session-replay";
71
71
 
72
72
  import { useAsyncCache } from "./common";
73
+ import { mountClickmapOverlay } from "../../../../clickmap";
73
74
  import { mountDevTool } from "../../../../dev-tool";
74
75
 
75
76
  let isReactServer = false;
@@ -702,6 +703,12 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
702
703
  if (isBrowserLike() && resolvedOptions.devTool !== false) {
703
704
  mountDevTool(this as any);
704
705
  }
706
+ if (isBrowserLike()) {
707
+ // Independent of the dev tool: the clickmap overlay only ever renders
708
+ // when a dashboard-minted token is handed over, so the listener is
709
+ // mounted unconditionally (the heavy UI is lazy-loaded on demand).
710
+ mountClickmapOverlay(this as any);
711
+ }
705
712
  }
706
713
 
707
714
  protected _initUniqueIdentifier() {
@@ -59,6 +59,8 @@ describe("EventTracker", () => {
59
59
 
60
60
  await advancePastFlush();
61
61
 
62
+ // Dead-click classification marks the buffered $click in place —
63
+ // exactly one click event either way.
62
64
  expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
63
65
  [
64
66
  "$page-view",
@@ -70,6 +72,291 @@ describe("EventTracker", () => {
70
72
  }
71
73
  });
72
74
 
75
+ it("emits a PostHog-style elements_chain plus scaled pointer coords for $click", async () => {
76
+ vi.useFakeTimers();
77
+ document.body.innerHTML = `
78
+ <main>
79
+ <section class="card panel">
80
+ <button id="save-btn" data-testid="save" aria-label="Save project">Save changes</button>
81
+ </section>
82
+ </main>
83
+ `;
84
+
85
+ const sentBodies: string[] = [];
86
+ const tracker = new EventTracker({
87
+ projectId: "internal",
88
+ sendBatch: async (body) => {
89
+ sentBodies.push(body);
90
+ return Result.ok(new Response());
91
+ },
92
+ });
93
+
94
+ try {
95
+ tracker.start();
96
+ const button = document.querySelector("#save-btn");
97
+ if (button == null) throw new Error("button missing");
98
+ button.dispatchEvent(new MouseEvent("click", {
99
+ bubbles: true,
100
+ clientX: 100,
101
+ clientY: 200,
102
+ }));
103
+
104
+ await advancePastFlush();
105
+
106
+ const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, data: Record<string, unknown> }[] };
107
+ const click = payload.events.find((event) => event.event_type === "$click");
108
+ if (click == null) throw new Error("no $click event captured");
109
+
110
+ // elements_chain encodes the target leaf plus a few ancestors. Leaf is
111
+ // first; segments are `;`-delimited. Assert against substrings rather
112
+ // than the full string so jsdom layout quirks don't make this flaky.
113
+ const chain = click.data.elements_chain;
114
+ expect(typeof chain).toBe("string");
115
+ expect(chain).toContain('button');
116
+ expect(chain).toContain('attr__id="save-btn"');
117
+ expect(chain).toContain('attr__data-testid="save"');
118
+ expect(chain).toContain('attr__aria-label="Save project"');
119
+ expect(chain).toContain('text="Save changes"');
120
+ // Ancestor section is in the chain too.
121
+ expect(chain).toContain("section");
122
+
123
+ // Pre-scaled coords land in clickmap_events.pointer_*. SCALE_FACTOR=16.
124
+ expect(click.data.x_scaled).toBe(Math.round(100 / 16));
125
+ expect(click.data.y_scaled).toBe(Math.round(200 / 16));
126
+ expect(click.data.client_y_scaled).toBe(Math.round(200 / 16));
127
+ expect(click.data.scale_factor).toBe(16);
128
+ expect(click.data.pointer_relative_x).toBeCloseTo(100 / window.innerWidth, 4);
129
+ expect(click.data.pointer_target_fixed).toBe(0);
130
+
131
+ // Legacy CSS selector still emitted for back-compat. The builder prefers
132
+ // data-testid over id, so we assert against that anchor rather than #id.
133
+ expect(click.data.selector).toContain('data-testid="save"');
134
+ expect(click.data.tag_name).toBe("button");
135
+ } finally {
136
+ tracker.stop();
137
+ }
138
+ });
139
+
140
+ it("ignores clicks inside the Hexclave dev tool", async () => {
141
+ vi.useFakeTimers();
142
+ document.body.innerHTML = `
143
+ <div id="__hexclave-dev-tool-root">
144
+ <button>Clickmap toolbar control</button>
145
+ </div>
146
+ `;
147
+
148
+ const sentBodies: string[] = [];
149
+ const tracker = new EventTracker({
150
+ projectId: "internal",
151
+ sendBatch: async (body) => {
152
+ sentBodies.push(body);
153
+ return Result.ok(new Response());
154
+ },
155
+ });
156
+
157
+ try {
158
+ tracker.start();
159
+ document.querySelector("button")?.dispatchEvent(new MouseEvent("click", {
160
+ bubbles: true,
161
+ clientX: 100,
162
+ clientY: 200,
163
+ }));
164
+
165
+ await advancePastFlush();
166
+
167
+ expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
168
+ [
169
+ "$page-view",
170
+ ]
171
+ `);
172
+ } finally {
173
+ tracker.stop();
174
+ }
175
+ });
176
+
177
+ it("flags pointer_target_fixed when the target sits under a fixed-position ancestor", async () => {
178
+ vi.useFakeTimers();
179
+ document.body.innerHTML = `
180
+ <header style="position: fixed; top: 0">
181
+ <button id="cta">Sign up</button>
182
+ </header>
183
+ `;
184
+
185
+ const sentBodies: string[] = [];
186
+ const tracker = new EventTracker({
187
+ projectId: "internal",
188
+ sendBatch: async (body) => {
189
+ sentBodies.push(body);
190
+ return Result.ok(new Response());
191
+ },
192
+ });
193
+
194
+ try {
195
+ tracker.start();
196
+ document.querySelector("#cta")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
197
+ await advancePastFlush();
198
+
199
+ const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, data: Record<string, unknown> }[] };
200
+ const click = payload.events.find((event) => event.event_type === "$click");
201
+ expect(click?.data.pointer_target_fixed).toBe(1);
202
+ } finally {
203
+ tracker.stop();
204
+ }
205
+ });
206
+
207
+ it("flags a click with no observable effect as dead on its single $click event", async () => {
208
+ vi.useFakeTimers();
209
+ document.body.innerHTML = "<button id=\"dead\">Does nothing</button>";
210
+
211
+ const sentBodies: string[] = [];
212
+ const tracker = new EventTracker({
213
+ projectId: "internal",
214
+ sendBatch: async (body) => {
215
+ sentBodies.push(body);
216
+ return Result.ok(new Response());
217
+ },
218
+ });
219
+
220
+ try {
221
+ tracker.start();
222
+ const clickAtMs = Date.now();
223
+ document.querySelector("#dead")?.dispatchEvent(new MouseEvent("click", {
224
+ bubbles: true,
225
+ clientX: 10,
226
+ clientY: 20,
227
+ }));
228
+
229
+ await advancePastFlush();
230
+
231
+ const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, event_at_ms: number, data: Record<string, unknown> }[] };
232
+ const clicks = payload.events.filter((event) => event.event_type === "$click");
233
+ expect(clicks).toHaveLength(1);
234
+ const click = clicks[0];
235
+
236
+ // One event per physical click: the buffered $click is marked dead in
237
+ // place, still timestamped at the original click rather than at
238
+ // classification time (~3s later).
239
+ expect(click.data.dead).toBe(1);
240
+ expect(click.event_at_ms).toBe(clickAtMs);
241
+ } finally {
242
+ tracker.stop();
243
+ }
244
+ });
245
+
246
+ it("does not flag a click as dead when it mutates the DOM", async () => {
247
+ vi.useFakeTimers();
248
+ document.body.innerHTML = "<button id=\"live\">Adds content</button><div id=\"out\"></div>";
249
+
250
+ const sentBodies: string[] = [];
251
+ const tracker = new EventTracker({
252
+ projectId: "internal",
253
+ sendBatch: async (body) => {
254
+ sentBodies.push(body);
255
+ return Result.ok(new Response());
256
+ },
257
+ });
258
+
259
+ try {
260
+ tracker.start();
261
+ const button = document.querySelector("#live");
262
+ if (button == null) throw new Error("button missing");
263
+ button.addEventListener("click", () => {
264
+ document.querySelector("#out")?.appendChild(document.createElement("p"));
265
+ });
266
+ button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
267
+ // Let the MutationObserver microtask run so the mutation is recorded
268
+ // before the dead-click sweeps start.
269
+ await Promise.resolve();
270
+
271
+ await advancePastFlush();
272
+
273
+ const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, data: Record<string, unknown> }[] };
274
+ const clicks = payload.events.filter((event) => event.event_type === "$click");
275
+ expect(clicks).toHaveLength(1);
276
+ expect(clicks[0].data.dead).toBeUndefined();
277
+ } finally {
278
+ tracker.stop();
279
+ }
280
+ });
281
+
282
+ it("drains held clicks as alive on pagehide so navigation clicks are never lost", async () => {
283
+ vi.useFakeTimers();
284
+ document.body.innerHTML = "<a id=\"nav\" href=\"/pricing\">Pricing</a>";
285
+
286
+ const sentBodies: string[] = [];
287
+ const tracker = new EventTracker({
288
+ projectId: "internal",
289
+ sendBatch: async (body) => {
290
+ sentBodies.push(body);
291
+ return Result.ok(new Response());
292
+ },
293
+ });
294
+
295
+ try {
296
+ tracker.start();
297
+ const clickAtMs = Date.now();
298
+ document.querySelector("#nav")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
299
+
300
+ // Navigation fires pagehide well before any classification sweep — the
301
+ // keepalive flush ships the still-unclassified click as a plain (alive)
302
+ // $click.
303
+ window.dispatchEvent(new Event("pagehide"));
304
+ await Promise.resolve();
305
+ await Promise.resolve();
306
+
307
+ const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, event_at_ms: number, data: Record<string, unknown> }[] };
308
+ const clicks = payload.events.filter((event) => event.event_type === "$click");
309
+ expect(clicks).toHaveLength(1);
310
+ expect(clicks[0].data.dead).toBeUndefined();
311
+ expect(clicks[0].event_at_ms).toBe(clickAtMs);
312
+ } finally {
313
+ tracker.stop();
314
+ }
315
+ });
316
+
317
+ it("holds an unclassified click out of a flush and ships it on the next one", async () => {
318
+ vi.useFakeTimers();
319
+ document.body.innerHTML = "<button id=\"late\">Late click</button>";
320
+
321
+ const sentBodies: string[] = [];
322
+ const tracker = new EventTracker({
323
+ projectId: "internal",
324
+ sendBatch: async (body) => {
325
+ sentBodies.push(body);
326
+ return Result.ok(new Response());
327
+ },
328
+ });
329
+
330
+ try {
331
+ tracker.start();
332
+ // Click 500ms before the 10s flush tick: classification cannot finish
333
+ // in time, so the flush must hold the click back rather than send it
334
+ // unclassified.
335
+ await vi.advanceTimersByTimeAsync(9_500);
336
+ document.querySelector("#late")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
337
+ await vi.advanceTimersByTimeAsync(500);
338
+
339
+ expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
340
+ [
341
+ "$page-view",
342
+ ]
343
+ `);
344
+
345
+ // By the next flush the sweep has classified it (dead — nothing
346
+ // observable happened) and it ships marked.
347
+ await vi.advanceTimersByTimeAsync(10_000);
348
+ const second = JSON.parse(sentBodies[1] ?? "{}") as { events: { event_type: string, data: Record<string, unknown> }[] };
349
+ expect(second.events.map((event) => event.event_type)).toMatchInlineSnapshot(`
350
+ [
351
+ "$click",
352
+ ]
353
+ `);
354
+ expect(second.events[0].data.dead).toBe(1);
355
+ } finally {
356
+ tracker.stop();
357
+ }
358
+ });
359
+
73
360
  it("captures client-side navigations when history is exposed as an accessor descriptor", async () => {
74
361
  vi.useFakeTimers();
75
362