@devaccel-labs/app-comments-toolbar 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +152 -18
  2. package/dist/react.js +516 -61
  3. package/dist/server.js +342 -14
  4. package/dist/types/capture/api.d.ts +30 -0
  5. package/dist/types/capture/editor.d.ts +10 -0
  6. package/dist/types/capture/ignore.d.ts +19 -0
  7. package/dist/types/capture/index.d.ts +1 -0
  8. package/dist/types/capture/picker.d.ts +9 -0
  9. package/dist/types/capture/picture.d.ts +29 -0
  10. package/dist/types/capture/surface.d.ts +39 -0
  11. package/dist/types/core/index.d.ts +2 -2
  12. package/dist/types/core/resolve.d.ts +2 -1
  13. package/dist/types/core/types.d.ts +11 -0
  14. package/dist/types/debug-context.d.ts +7 -0
  15. package/dist/types/dictation.d.ts +9 -0
  16. package/dist/types/embed/controller.d.ts +61 -0
  17. package/dist/types/embed/index.d.ts +1 -0
  18. package/dist/types/embed/options.d.ts +35 -0
  19. package/dist/types/help/index.d.ts +1 -0
  20. package/dist/types/help/panel-index.d.ts +1 -0
  21. package/dist/types/help/panel-styles.d.ts +1 -0
  22. package/dist/types/help/panel.d.ts +4 -0
  23. package/dist/types/help/pill-styles.d.ts +11 -0
  24. package/dist/types/help/pill.d.ts +44 -0
  25. package/dist/types/help/react.d.ts +12 -0
  26. package/dist/types/help/requests.d.ts +2 -0
  27. package/dist/types/help/types.d.ts +119 -0
  28. package/dist/types/help/wire.d.ts +61 -0
  29. package/dist/types/native-wire.d.ts +63 -0
  30. package/dist/types/native.d.ts +43 -0
  31. package/dist/types/react.d.ts +8 -1
  32. package/dist/types/replay.d.ts +6 -0
  33. package/dist/types/runtime/api.d.ts +250 -0
  34. package/dist/types/runtime/attachment-model.d.ts +16 -0
  35. package/dist/types/runtime/attachments.d.ts +43 -7
  36. package/dist/types/runtime/boot.d.ts +39 -0
  37. package/dist/types/runtime/browser-label.d.ts +5 -0
  38. package/dist/types/runtime/build-id.d.ts +7 -0
  39. package/dist/types/runtime/capture-ui.d.ts +45 -21
  40. package/dist/types/runtime/clarity-model.d.ts +260 -0
  41. package/dist/types/runtime/clarity-ui.d.ts +62 -0
  42. package/dist/types/runtime/client.d.ts +6 -197
  43. package/dist/types/runtime/composer-size.d.ts +30 -0
  44. package/dist/types/runtime/dashboard.d.ts +17 -2
  45. package/dist/types/runtime/debug-context-core.d.ts +88 -0
  46. package/dist/types/runtime/debug-context-host.d.ts +39 -0
  47. package/dist/types/runtime/debug-context-model.d.ts +352 -0
  48. package/dist/types/runtime/debug-context-redact.d.ts +48 -0
  49. package/dist/types/runtime/debug-context.d.ts +36 -0
  50. package/dist/types/runtime/details-model.d.ts +7 -0
  51. package/dist/types/runtime/details.d.ts +7 -5
  52. package/dist/types/runtime/dictation-engine.d.ts +98 -0
  53. package/dist/types/runtime/dictation-model.d.ts +28 -0
  54. package/dist/types/runtime/dictation-ui.d.ts +56 -0
  55. package/dist/types/runtime/dictation.d.ts +66 -0
  56. package/dist/types/runtime/draft-dock.d.ts +32 -0
  57. package/dist/types/runtime/draft-model.d.ts +104 -0
  58. package/dist/types/runtime/draft-store.d.ts +15 -0
  59. package/dist/types/runtime/edits.d.ts +1 -1
  60. package/dist/types/runtime/font.d.ts +16 -0
  61. package/dist/types/runtime/format.d.ts +30 -1
  62. package/dist/types/runtime/host-watch.d.ts +20 -0
  63. package/dist/types/runtime/icons.d.ts +16 -2
  64. package/dist/types/runtime/index.d.ts +3 -1
  65. package/dist/types/runtime/ink.d.ts +8 -0
  66. package/dist/types/runtime/kind-model.d.ts +14 -0
  67. package/dist/types/runtime/live-model.d.ts +25 -0
  68. package/dist/types/runtime/loader.d.ts +5 -11
  69. package/dist/types/runtime/markup-editor.d.ts +54 -0
  70. package/dist/types/runtime/markup-model.d.ts +126 -0
  71. package/dist/types/runtime/markup.d.ts +69 -0
  72. package/dist/types/runtime/mention-parse.d.ts +18 -0
  73. package/dist/types/runtime/mentions.d.ts +4 -4
  74. package/dist/types/runtime/page-background.d.ts +9 -0
  75. package/dist/types/runtime/page-image.d.ts +123 -0
  76. package/dist/types/runtime/placement.d.ts +6 -3
  77. package/dist/types/runtime/record.d.ts +18 -2
  78. package/dist/types/runtime/related.d.ts +1 -1
  79. package/dist/types/runtime/replay-model.d.ts +80 -0
  80. package/dist/types/runtime/replay.d.ts +5 -38
  81. package/dist/types/runtime/runtime.d.ts +6 -0
  82. package/dist/types/runtime/screen.d.ts +47 -0
  83. package/dist/types/runtime/screenshot.d.ts +60 -6
  84. package/dist/types/runtime/session-source.d.ts +45 -0
  85. package/dist/types/runtime/session.d.ts +20 -18
  86. package/dist/types/runtime/shield.d.ts +9 -0
  87. package/dist/types/runtime/shot.d.ts +45 -0
  88. package/dist/types/runtime/sketch.d.ts +26 -0
  89. package/dist/types/runtime/styles.d.ts +1 -1
  90. package/dist/types/runtime/sync.d.ts +1 -0
  91. package/dist/types/runtime/thread-model.d.ts +18 -0
  92. package/dist/types/runtime/ticket-form-model.d.ts +57 -0
  93. package/dist/types/runtime/ticket-form.d.ts +9 -0
  94. package/dist/types/runtime/toolbar.d.ts +37 -1
  95. package/dist/types/runtime/ui-scale.d.ts +17 -0
  96. package/dist/types/runtime/update.d.ts +37 -0
  97. package/dist/types/runtime/use-debug-context.d.ts +27 -0
  98. package/dist/types/server/index.d.ts +22 -0
  99. package/dist/types/server/support.d.ts +20 -0
  100. package/dist/types/server.d.ts +2 -0
  101. package/dist/types/vite/plugin.d.ts +15 -0
  102. package/dist/vite.js +121 -41
  103. package/package.json +26 -13
  104. package/src/capture/api.ts +128 -0
  105. package/src/capture/editor.tsx +178 -0
  106. package/src/capture/ignore.ts +38 -0
  107. package/src/capture/index.ts +27 -0
  108. package/src/capture/picker.tsx +171 -0
  109. package/src/capture/picture.ts +90 -0
  110. package/src/capture/surface.tsx +185 -0
  111. package/src/compiler/annotate.ts +202 -0
  112. package/src/compiler/babel.d.ts +11 -0
  113. package/src/compiler/enabled.ts +55 -0
  114. package/src/compiler/index.ts +2 -0
  115. package/src/core/index.ts +8 -0
  116. package/src/core/resolve.ts +307 -0
  117. package/src/core/types.ts +58 -0
  118. package/src/debug-context.ts +47 -0
  119. package/src/dictation.ts +17 -0
  120. package/src/embed/controller.ts +215 -0
  121. package/src/embed/index.ts +68 -0
  122. package/src/embed/options.ts +100 -0
  123. package/src/help/index.ts +38 -0
  124. package/src/help/panel-index.ts +17 -0
  125. package/src/help/panel-styles.ts +123 -0
  126. package/src/help/panel.ts +840 -0
  127. package/src/help/pill-styles.ts +75 -0
  128. package/src/help/pill.ts +338 -0
  129. package/src/help/react.tsx +143 -0
  130. package/src/help/requests.ts +111 -0
  131. package/src/help/types.ts +124 -0
  132. package/src/help/wire.ts +55 -0
  133. package/src/native-wire.ts +124 -0
  134. package/src/native.ts +282 -0
  135. package/src/react.ts +27 -0
  136. package/src/replay.ts +13 -0
  137. package/src/runtime/api.ts +531 -0
  138. package/src/runtime/attachment-model.ts +42 -0
  139. package/src/runtime/attachments.tsx +1260 -0
  140. package/src/runtime/boot.ts +95 -0
  141. package/src/runtime/browser-label.ts +31 -0
  142. package/src/runtime/build-id.ts +10 -0
  143. package/src/runtime/capture-ui.tsx +306 -0
  144. package/src/runtime/capture.ts +508 -0
  145. package/src/runtime/clarity-model.ts +634 -0
  146. package/src/runtime/clarity-ui.tsx +454 -0
  147. package/src/runtime/client.ts +151 -0
  148. package/src/runtime/composer-size.tsx +146 -0
  149. package/src/runtime/dashboard.tsx +1130 -0
  150. package/src/runtime/debug-context-core.ts +681 -0
  151. package/src/runtime/debug-context-host.ts +56 -0
  152. package/src/runtime/debug-context-model.ts +1319 -0
  153. package/src/runtime/debug-context-redact.ts +259 -0
  154. package/src/runtime/debug-context.ts +613 -0
  155. package/src/runtime/details-model.ts +30 -0
  156. package/src/runtime/details.tsx +395 -0
  157. package/src/runtime/dictation-engine.ts +616 -0
  158. package/src/runtime/dictation-model.ts +79 -0
  159. package/src/runtime/dictation-ui.tsx +363 -0
  160. package/src/runtime/dictation.ts +271 -0
  161. package/src/runtime/dock.ts +108 -0
  162. package/src/runtime/draft-dock.tsx +124 -0
  163. package/src/runtime/draft-model.ts +251 -0
  164. package/src/runtime/draft-store.ts +149 -0
  165. package/src/runtime/edits.tsx +165 -0
  166. package/src/runtime/font.ts +42 -0
  167. package/src/runtime/format.ts +129 -0
  168. package/src/runtime/freshness.ts +23 -0
  169. package/src/runtime/handoff.ts +76 -0
  170. package/src/runtime/host-watch.ts +64 -0
  171. package/src/runtime/icons.tsx +295 -0
  172. package/src/runtime/idle.ts +20 -0
  173. package/src/runtime/index.ts +11 -0
  174. package/src/runtime/ink.ts +6 -0
  175. package/src/runtime/kind-model.ts +20 -0
  176. package/src/runtime/live-model.ts +32 -0
  177. package/src/runtime/loader.tsx +87 -0
  178. package/src/runtime/markup-editor.tsx +1333 -0
  179. package/src/runtime/markup-model.ts +485 -0
  180. package/src/runtime/markup.ts +382 -0
  181. package/src/runtime/members.ts +58 -0
  182. package/src/runtime/mention-parse.ts +56 -0
  183. package/src/runtime/mentions.tsx +205 -0
  184. package/src/runtime/page-background.ts +51 -0
  185. package/src/runtime/page-image.ts +1592 -0
  186. package/src/runtime/placement.ts +336 -0
  187. package/src/runtime/record.ts +402 -0
  188. package/src/runtime/redact.ts +30 -0
  189. package/src/runtime/related.ts +34 -0
  190. package/src/runtime/render-page.ts +2 -0
  191. package/src/runtime/replay-model.ts +232 -0
  192. package/src/runtime/replay.ts +173 -0
  193. package/src/runtime/runtime-version.ts +7 -0
  194. package/src/runtime/runtime.tsx +51 -0
  195. package/src/runtime/screen.ts +182 -0
  196. package/src/runtime/screenshot.ts +260 -0
  197. package/src/runtime/session-source.ts +112 -0
  198. package/src/runtime/session.ts +108 -0
  199. package/src/runtime/shield.ts +34 -0
  200. package/src/runtime/shot.tsx +422 -0
  201. package/src/runtime/sketch.ts +139 -0
  202. package/src/runtime/styles.ts +1541 -0
  203. package/src/runtime/sync.ts +300 -0
  204. package/src/runtime/thread-model.ts +71 -0
  205. package/src/runtime/ticket-form-model.ts +128 -0
  206. package/src/runtime/ticket-form.tsx +342 -0
  207. package/src/runtime/toolbar.tsx +4427 -0
  208. package/src/runtime/ui-scale.ts +75 -0
  209. package/src/runtime/update.ts +98 -0
  210. package/src/runtime/use-debug-context.ts +81 -0
  211. package/src/server/index.ts +133 -0
  212. package/src/server/support.ts +499 -0
  213. package/src/server.ts +4 -0
  214. package/src/vite/plugin.ts +102 -0
  215. package/src/vite.ts +1 -0
  216. package/dist/chunks/chunk-CSKXLF33.js +0 -153
  217. package/dist/chunks/chunk-HUM5FQII.js +0 -160
  218. package/dist/chunks/render-page-76MLIT6M.js +0 -1421
  219. package/dist/chunks/runtime-JX75TKER.js +0 -9267
  220. package/dist/chunks/screenshot-66ARTBGR.js +0 -12
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The toolbar's dictation engine, for a connection's own composers (an inbox's reply box). The
3
+ * `./dictation` export points at this source for the monorepo only: it is not built into `dist`, so the npm
4
+ * package does not ship it. `ApiError` is what a `transcribe` callback throws so the engine can tell a retry from a
5
+ * stop (`rate_limited`, `dictation_unavailable`, 401, 403).
6
+ */
7
+ export {
8
+ Dictation,
9
+ dictationSupported,
10
+ idleDictation,
11
+ MAX_TAKE_MS,
12
+ voiceRecordingSupported,
13
+ type DictationDeps,
14
+ type DictationState,
15
+ } from "./runtime/dictation-engine.js";
16
+ export { dictationPrompt, insertDictated } from "./runtime/dictation.js";
17
+ export { ApiError } from "./runtime/client.js";
@@ -0,0 +1,215 @@
1
+ /**
2
+ * The script-tag loader's state: one toolbar per page, brought up by the same steps as `<CommentToolbar>`
3
+ * (`../runtime/boot.ts`), and the three things a single-page app may ask of it afterwards.
4
+ *
5
+ * ## Which service the toolbar may come from
6
+ *
7
+ * `<CommentToolbar>` imports the toolbar from the service the host's own session route names (`apiUrl`), and
8
+ * so does this loader: nothing on the page, and no attribute of the tag, can name another one. The loader also
9
+ * knows where it was itself loaded from. The two are the same service in every ordinary install, so a session
10
+ * that names another origin than the loader's own is refused: nothing is imported and nothing mounts. The one
11
+ * way to allow it is for the session route to say so itself, by naming the loader's origin in `loaderOrigin`.
12
+ *
13
+ * ## Following the host's sign-in
14
+ *
15
+ * While the toolbar is on the page it follows the host's sign-in itself (`ToolbarApp`). While it is not, because
16
+ * nobody was signed in when the page loaded, this loader asks the session route again at the same moments
17
+ * (`watchHost`), so a sign-in that loads no page still brings the toolbar. Not after the host's own `unmount()`.
18
+ */
19
+ import { bootToolbar, type Runtime, type Wait } from "../runtime/boot.js";
20
+ import { watchHost } from "../runtime/host-watch.js";
21
+ import type { RuntimeProps, ToolbarHandle } from "../runtime/runtime.js";
22
+ import type { ScreenRule } from "../runtime/screen.js";
23
+ import type { GetSession, Session } from "../runtime/session-source.js";
24
+ import { sessionFor } from "../runtime/session.js";
25
+ import type { ToolbarPosition, ToolbarTheme } from "../runtime/toolbar.js";
26
+ import type { DebugContextConfig } from "../runtime/debug-context-model.js";
27
+ import { isPosition, isTheme, POSITION_NAMES, THEME_NAMES, type EmbedOptions } from "./options.js";
28
+
29
+ /** What `window.CommentTool` is. */
30
+ export type CommentToolApi = {
31
+ /**
32
+ * Asks the session route again. Mounts the toolbar when comments are now on and it is not on the page,
33
+ * removes it when they are now off, and has a toolbar that stays ask again too. Resolves once that is done.
34
+ * Call it after a sign-in or a sign-out that did not reload the page.
35
+ */
36
+ refresh(): Promise<void>;
37
+ /** Removes the toolbar. `refresh()` brings it back. */
38
+ unmount(): void;
39
+ /**
40
+ * `navigate` is the router's client-side navigation, as the component's `navigate` prop (null takes it away
41
+ * again); `position`, `theme` and `screen` are the component's props of those names (null takes `screen`
42
+ * away). A wrong value throws a TypeError and changes nothing.
43
+ */
44
+ configure(options: {
45
+ navigate?: ((href: string) => void) | null;
46
+ position?: ToolbarPosition;
47
+ theme?: ToolbarTheme;
48
+ screen?: ScreenRule | null;
49
+ /** The component's `debugContext` prop: `true`, `false`, or its options. Null, or any other value, turns it off. */
50
+ debugContext?: DebugContextConfig | null;
51
+ }): void;
52
+ };
53
+
54
+ export type EmbedInput = {
55
+ /** The origin `embed.js` was loaded from. */
56
+ serviceOrigin: string;
57
+ options: EmbedOptions;
58
+ /** The page-wide session source for a path. Default: the one the toolbar itself uses. */
59
+ sessions?: (path: string) => GetSession;
60
+ load?: (apiUrl: string) => Promise<Runtime>;
61
+ /** When the first attempt starts. Default: when the page is idle. */
62
+ wait?: Wait;
63
+ };
64
+
65
+ const now: Wait = (task) => {
66
+ task();
67
+ return () => {};
68
+ };
69
+
70
+ function originOf(url: string): string | null {
71
+ try {
72
+ return new URL(url).origin;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ export function createEmbed(input: EmbedInput): CommentToolApi & { start(): void } {
79
+ const { serviceOrigin, options } = input;
80
+ const source = (input.sessions ?? sessionFor)(options.sessionPath);
81
+ let navigate: ((href: string) => void) | null = null;
82
+ let position = options.position;
83
+ let theme = options.theme;
84
+ let screen: ScreenRule | undefined = options.screen;
85
+ let debugContext: DebugContextConfig | undefined;
86
+ /** Raised to have a toolbar on the page ask the session route now. */
87
+ let hostCheck = 0;
88
+ /** The host took the toolbar off with `unmount()`: only its own `refresh()` brings it back. */
89
+ let held = false;
90
+
91
+ let handle: ToolbarHandle | null = null;
92
+ let container: HTMLElement | null = null;
93
+ /** Calls off the attempt under way, or removes the toolbar it mounted. */
94
+ let stop: (() => void) | null = null;
95
+ /** Raised by every `refresh()` and `unmount()`: an older `refresh()` that is still waiting gives up. */
96
+ let generation = 0;
97
+ let refused: string | null = null;
98
+
99
+ const accepts = (session: Session) =>
100
+ originOf(session.apiUrl) === serviceOrigin ||
101
+ (session.loaderOrigin !== undefined && originOf(session.loaderOrigin) === serviceOrigin);
102
+
103
+ /** The session, or null when comments are off or the session names a service this loader did not come from. */
104
+ const getSession: GetSession = async (request) => {
105
+ const session = await source(request);
106
+ if (!session || accepts(session)) return session;
107
+ if (refused !== session.apiUrl) {
108
+ refused = session.apiUrl;
109
+ console.warn(
110
+ `[comment-service] The session route names ${session.apiUrl}, but embed.js was loaded from ${serviceOrigin}. The toolbar was not loaded. Load embed.js from the service the session route names.`,
111
+ );
112
+ }
113
+ return null;
114
+ };
115
+
116
+ const props = (): RuntimeProps => ({
117
+ sessionPath: options.sessionPath,
118
+ pollMs: options.pollMs,
119
+ position,
120
+ theme,
121
+ navigate: navigate ? (href) => navigate?.(href) : undefined,
122
+ screen,
123
+ debugContext,
124
+ hostCheck,
125
+ getSession,
126
+ });
127
+
128
+ const mounted = () => handle !== null && container?.isConnected === true;
129
+
130
+ const teardown = () => {
131
+ stop?.();
132
+ stop = null;
133
+ handle = null;
134
+ container = null;
135
+ };
136
+
137
+ const boot = (wait: Wait | undefined, onSettled?: () => void) => {
138
+ stop = bootToolbar({
139
+ getSession: () => getSession(),
140
+ props,
141
+ onMount: (mounted, element) => {
142
+ handle = mounted;
143
+ container = element;
144
+ },
145
+ onSettled,
146
+ wait,
147
+ load: input.load,
148
+ });
149
+ };
150
+
151
+ const refresh = async () => {
152
+ const mine = ++generation;
153
+ const session = await getSession({ refresh: true }).catch(() => null);
154
+ if (mine !== generation) return;
155
+ if (!session) {
156
+ teardown();
157
+ return;
158
+ }
159
+ // Still on the page: it takes the answer just read, and turns itself back on if it had gone off.
160
+ if (mounted()) {
161
+ hostCheck += 1;
162
+ handle?.update(props());
163
+ return;
164
+ }
165
+ // Not mounted yet, or the host replaced the part of the page it was in. One attempt, replacing any other.
166
+ teardown();
167
+ await new Promise<void>((settled) => boot(now, settled));
168
+ };
169
+
170
+ return {
171
+ start() {
172
+ boot(input.wait);
173
+ watchHost(() => {
174
+ if (!held && !mounted()) void refresh();
175
+ });
176
+ },
177
+ refresh() {
178
+ held = false;
179
+ return refresh();
180
+ },
181
+ unmount() {
182
+ held = true;
183
+ generation += 1;
184
+ teardown();
185
+ },
186
+ configure(next) {
187
+ if (next.position !== undefined && !isPosition(next.position)) {
188
+ throw new TypeError(`CommentTool.configure: position must be one of ${POSITION_NAMES}.`);
189
+ }
190
+ if (next.theme !== undefined && !isTheme(next.theme)) {
191
+ throw new TypeError(`CommentTool.configure: theme must be one of ${THEME_NAMES}.`);
192
+ }
193
+ if (next.navigate != null && typeof next.navigate !== "function") {
194
+ throw new TypeError("CommentTool.configure: navigate must be a function.");
195
+ }
196
+ const rule = next.screen;
197
+ if (
198
+ rule != null &&
199
+ typeof rule !== "function" &&
200
+ !(Array.isArray(rule.params) && rule.params.every((name) => typeof name === "string"))
201
+ ) {
202
+ throw new TypeError(
203
+ "CommentTool.configure: screen must be { params: [names] } or a function.",
204
+ );
205
+ }
206
+ if (next.position !== undefined) position = next.position;
207
+ if (next.theme !== undefined) theme = next.theme;
208
+ if (next.navigate !== undefined) navigate = next.navigate;
209
+ if (rule !== undefined) screen = rule ?? undefined;
210
+ // Not checked here, to keep this loader small: the toolbar treats anything but `true` or an object as off.
211
+ if (next.debugContext !== undefined) debugContext = next.debugContext ?? undefined;
212
+ handle?.update(props());
213
+ },
214
+ };
215
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `/v1/toolbar/embed.js`: the toolbar's loader for a host that installs nothing.
3
+ *
4
+ * <script src="https://<service>/v1/toolbar/embed.js" async></script>
5
+ *
6
+ * It does what `<CommentToolbar>` does, in the same order: keep an invite or thread link from the address,
7
+ * wait for the page to be idle, ask the host's session route with the page's cookies, and only when comments
8
+ * are on import the toolbar from the service and mount it. `scripts/build-toolbar.mjs` builds this file next
9
+ * to the runtime, so the service serves both and a deploy of the service updates both.
10
+ *
11
+ * A classic script, on purpose: it reads its own `<script>` tag (`document.currentScript`) for the service's
12
+ * origin and for its options, which a module script cannot do.
13
+ */
14
+ import { captureHandoff } from "../runtime/handoff.js";
15
+ import { createEmbed, type CommentToolApi } from "./controller.js";
16
+ import { readOptions } from "./options.js";
17
+
18
+ const GLOBAL = "CommentTool";
19
+ /** Dispatched on `document` once `window.CommentTool` exists, for code that ran before this script loaded. */
20
+ const READY_EVENT = "comment-tool:ready";
21
+
22
+ /** What the global is when the tag was refused: the host's calls still work, and do nothing. */
23
+ const inert: CommentToolApi = {
24
+ refresh: () => Promise.resolve(),
25
+ unmount() {},
26
+ configure() {},
27
+ };
28
+
29
+ function install(): void {
30
+ const page = window as unknown as Record<string, Partial<CommentToolApi> | undefined>;
31
+ // A second tag, or the same tag run again by a router: the first one's toolbar is the page's toolbar.
32
+ if (typeof page[GLOBAL]?.refresh === "function") return;
33
+
34
+ const script = document.currentScript;
35
+ if (!(script instanceof HTMLScriptElement) || !script.src) {
36
+ console.error(
37
+ '[comment-service] embed.js must be loaded by its own <script src="…/v1/toolbar/embed.js"> tag, not as a module and not inline. The toolbar was not loaded.',
38
+ );
39
+ return;
40
+ }
41
+
42
+ const read = readOptions(script.dataset, window.location.href);
43
+ let api: CommentToolApi = inert;
44
+ let start: (() => void) | null = null;
45
+ if (read.ok) {
46
+ captureHandoff();
47
+ const embed = createEmbed({
48
+ serviceOrigin: new URL(script.src).origin,
49
+ options: read.options,
50
+ });
51
+ api = { refresh: embed.refresh, unmount: embed.unmount, configure: embed.configure };
52
+ start = embed.start;
53
+ } else {
54
+ console.error(`[comment-service] embed.js: ${read.problem} The toolbar was not loaded.`);
55
+ }
56
+
57
+ page[GLOBAL] = api;
58
+ document.dispatchEvent(new Event(READY_EVENT));
59
+ if (!start) return;
60
+ // The toolbar mounts at the end of <body>, so the page's own markup comes first, as it does for a component.
61
+ if (document.readyState === "loading") {
62
+ document.addEventListener("DOMContentLoaded", start, { once: true });
63
+ } else {
64
+ start();
65
+ }
66
+ }
67
+
68
+ install();
@@ -0,0 +1,100 @@
1
+ /**
2
+ * What a host may set on the loader's `<script>` tag, and with `CommentTool.configure`. The same four things
3
+ * `<CommentToolbar>` takes as props. There the compiler refuses a wrong value; a `data-` attribute is only
4
+ * text, so the loader checks it and loads nothing when one is wrong.
5
+ */
6
+ import type { ToolbarPosition, ToolbarTheme } from "../runtime/toolbar.js";
7
+
8
+ export const DEFAULT_SESSION_PATH = "/api/comment-service/session";
9
+
10
+ // Keyed by the component's own types: a position or theme added there fails to compile here until it is listed.
11
+ const POSITIONS: Record<ToolbarPosition, true> = {
12
+ "bottom-left": true,
13
+ "bottom-center": true,
14
+ "bottom-right": true,
15
+ };
16
+ const THEMES: Record<ToolbarTheme, true> = { light: true, dark: true, system: true };
17
+
18
+ export type EmbedOptions = {
19
+ sessionPath: string;
20
+ position?: ToolbarPosition;
21
+ theme?: ToolbarTheme;
22
+ pollMs?: number;
23
+ /** `data-screen-params`: the query parameters that select a screen of the app. */
24
+ screen?: { params: string[] };
25
+ };
26
+
27
+ export type ReadOptions = { ok: true; options: EmbedOptions } | { ok: false; problem: string };
28
+
29
+ export function isPosition(value: unknown): value is ToolbarPosition {
30
+ return typeof value === "string" && Object.hasOwn(POSITIONS, value);
31
+ }
32
+
33
+ export function isTheme(value: unknown): value is ToolbarTheme {
34
+ return typeof value === "string" && Object.hasOwn(THEMES, value);
35
+ }
36
+
37
+ const oneOf = (names: Record<string, true>) => Object.keys(names).join(", ");
38
+ export const POSITION_NAMES = oneOf(POSITIONS);
39
+ export const THEME_NAMES = oneOf(THEMES);
40
+
41
+ /** True for a path on the page's own origin: the session request must carry the page's cookies, nobody else's. */
42
+ function ownPath(value: string, pageUrl: string): boolean {
43
+ if (!value.startsWith("/")) return false;
44
+ try {
45
+ return new URL(value, pageUrl).origin === new URL(pageUrl).origin;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Reads `data-session-path`, `data-position`, `data-theme`, `data-poll-ms` and `data-screen-params` (as
53
+ * `dataset` names them). An
54
+ * attribute that is absent takes the component's default; one that is present and wrong is a refusal that names
55
+ * it. Other `data-` attributes are the host's own and are not read.
56
+ */
57
+ export function readOptions(
58
+ data: Readonly<Record<string, string | undefined>>,
59
+ pageUrl: string,
60
+ ): ReadOptions {
61
+ const refuse = (attribute: string, rule: string, value: string): ReadOptions => ({
62
+ ok: false,
63
+ problem: `${attribute} must be ${rule}. It is ${JSON.stringify(value)}.`,
64
+ });
65
+ const options: EmbedOptions = { sessionPath: DEFAULT_SESSION_PATH };
66
+
67
+ const sessionPath = data["sessionPath"];
68
+ if (sessionPath !== undefined) {
69
+ if (!ownPath(sessionPath, pageUrl)) {
70
+ return refuse("data-session-path", "a path on this page's own origin", sessionPath);
71
+ }
72
+ options.sessionPath = sessionPath;
73
+ }
74
+ const position = data["position"];
75
+ if (position !== undefined) {
76
+ if (!isPosition(position)) return refuse("data-position", `one of ${POSITION_NAMES}`, position);
77
+ options.position = position;
78
+ }
79
+ const theme = data["theme"];
80
+ if (theme !== undefined) {
81
+ if (!isTheme(theme)) return refuse("data-theme", `one of ${THEME_NAMES}`, theme);
82
+ options.theme = theme;
83
+ }
84
+ const pollMs = data["pollMs"];
85
+ if (pollMs !== undefined) {
86
+ if (!/^[1-9]\d{0,8}$/.test(pollMs)) {
87
+ return refuse("data-poll-ms", "a whole number of milliseconds above zero", pollMs);
88
+ }
89
+ options.pollMs = Number(pollMs);
90
+ }
91
+ const screenParams = data["screenParams"];
92
+ if (screenParams !== undefined) {
93
+ const params = screenParams.split(/[\s,]+/).filter(Boolean);
94
+ if (!params.length) {
95
+ return refuse("data-screen-params", "query parameter names", screenParams);
96
+ }
97
+ options.screen = { params };
98
+ }
99
+ return { ok: true, options };
100
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `/v1/toolbar/help.js`: the "Need help?" pill, for a host page to show every signed-in person.
3
+ *
4
+ * <script src="https://<service>/v1/toolbar/help.js" async
5
+ * onload="CommentToolHelp.mount(CommentToolHelp.supportRequestsAt('/api/support'))"></script>
6
+ *
7
+ * A classic script, like `embed.js` and `capture.js`. It defines `window.CommentToolHelp` and is only the
8
+ * pill: the panel (`help-panel.js`) and the capture script (`capture.js`) come from beside this file the
9
+ * first time the pill is hovered, focused or opened. It asks no session route, reads no stored sign-in and
10
+ * calls nothing but what the host's two hooks call.
11
+ */
12
+ import { createHelp } from "./pill.js";
13
+ import type { CommentToolHelpApi } from "./types.js";
14
+
15
+ const GLOBAL = "CommentToolHelp";
16
+ const FILE = "/v1/toolbar/help.js";
17
+ /** Dispatched on `document` once `window.CommentToolHelp` exists, for code that does not wait on the tag. */
18
+ const READY_EVENT = "comment-tool-help:ready";
19
+
20
+ /** The folder this script was served from: its siblings are fetched from there. */
21
+ function base(): string {
22
+ const current = document.currentScript;
23
+ const tag =
24
+ current instanceof HTMLScriptElement && current.src
25
+ ? current
26
+ : document.querySelector<HTMLScriptElement>(`script[src*="${FILE}"]`);
27
+ return tag?.src ? new URL(".", tag.src).href : new URL("/v1/toolbar/", window.location.href).href;
28
+ }
29
+
30
+ function install(): void {
31
+ const page = window as unknown as Record<string, Partial<CommentToolHelpApi> | undefined>;
32
+ // A second tag: the first one's object stays, with the pill it has mounted.
33
+ if (typeof page[GLOBAL]?.mount === "function") return;
34
+ page[GLOBAL] = createHelp({ base: base() });
35
+ document.dispatchEvent(new Event(READY_EVENT));
36
+ }
37
+
38
+ install();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `/v1/toolbar/help-panel.js`: the panel of the "Need help?" pill. `help.js` adds this script the first time
3
+ * the pill is reached for and then calls `window.CommentToolHelpPanel.create`. A host never loads it itself,
4
+ * and what it defines is the pill's to call, not a host's.
5
+ */
6
+ import { createPanel } from "./panel.js";
7
+ import type { HelpPanelApi } from "./pill.js";
8
+
9
+ const GLOBAL = "CommentToolHelpPanel";
10
+
11
+ function install(): void {
12
+ const page = window as unknown as Record<string, Partial<HelpPanelApi> | undefined>;
13
+ if (typeof page[GLOBAL]?.create === "function") return;
14
+ page[GLOBAL] = { create: createPanel };
15
+ }
16
+
17
+ install();
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The panel's CSS, on the reset and the colours of `./pill-styles.ts`. A card anchored above the pill; a sheet
3
+ * along the bottom of the window when the window is narrow.
4
+ */
5
+ import { HELP_Z_INDEX } from "./pill-styles.js";
6
+
7
+ export const HELP_PANEL_CSS = `
8
+ .cth .cth-panel {
9
+ position: fixed; z-index: ${HELP_Z_INDEX + 1};
10
+ bottom: calc(var(--cth-edge) + 52px + env(safe-area-inset-bottom, 0px) + var(--cth-lift, 0px));
11
+ display: flex; flex-direction: column; width: 380px; max-width: calc(100vw - 2 * var(--cth-edge));
12
+ max-height: min(620px, calc(100vh - var(--cth-edge) - 76px - var(--cth-lift, 0px)));
13
+ border-radius: 18px; background: var(--cth-card); color: var(--cth-ink); overflow: hidden;
14
+ box-shadow: 0 0 0 1px var(--cth-ring), var(--cth-shadow);
15
+ }
16
+ .cth[data-position="bottom-right"] .cth-panel { right: calc(var(--cth-edge) + env(safe-area-inset-right, 0px)); }
17
+ .cth[data-position="bottom-left"] .cth-panel { left: calc(var(--cth-edge) + env(safe-area-inset-left, 0px)); }
18
+ .cth .cth-panel:focus { outline: none; }
19
+ .cth .cth-content { display: contents; }
20
+ .cth .cth-head { display: flex; align-items: center; gap: 8px; padding: 14px 12px 10px 18px; }
21
+ .cth .cth-title { display: block; flex: 1; min-width: 0; font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
22
+ .cth .cth-x {
23
+ display: inline-flex; flex: none; align-items: center; justify-content: center; width: 28px; height: 28px;
24
+ border-radius: 50%; color: var(--cth-muted); cursor: pointer;
25
+ }
26
+ .cth .cth-x:hover { background: var(--cth-fill); color: var(--cth-ink); }
27
+ .cth .cth-icon { display: block; flex: none; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
28
+ .cth .cth-tabs { display: flex; flex: none; margin: 0 18px 10px; padding: 3px; border-radius: 10px; background: var(--cth-track); }
29
+ .cth .cth-tab {
30
+ display: block; flex: 1; padding: 6px 8px; border-radius: 8px; color: var(--cth-muted);
31
+ font-size: 12.5px; font-weight: 600; text-align: center; cursor: pointer;
32
+ }
33
+ .cth .cth-tab[aria-selected="true"] { background: var(--cth-raised); color: var(--cth-ink); box-shadow: 0 1px 2px rgba(20, 20, 30, 0.14); }
34
+ .cth .cth-body {
35
+ display: flex; flex: 1; flex-direction: column; gap: 10px; min-height: 0; padding: 2px 18px 16px;
36
+ overflow-y: auto; overscroll-behavior: contain;
37
+ }
38
+ .cth .cth-form { display: flex; flex-direction: column; gap: 10px; }
39
+ .cth .cth-q { display: block; font-size: 13px; font-weight: 600; }
40
+ .cth .cth-text {
41
+ display: block; width: 100%; min-height: 104px; max-height: 220px; padding: 10px 12px; border-radius: 12px;
42
+ background: var(--cth-field); box-shadow: inset 0 0 0 1px var(--cth-line); font-size: 14px; line-height: 1.45;
43
+ white-space: pre-wrap; overflow-wrap: anywhere; overflow: auto; resize: vertical; cursor: text;
44
+ }
45
+ .cth .cth-text::placeholder { color: var(--cth-muted); }
46
+ .cth .cth-text:focus { outline: none; box-shadow: inset 0 0 0 2px var(--cth-accent); }
47
+ .cth .cth-text[readonly] { opacity: 0.7; }
48
+ .cth .cth-count { display: block; margin-top: -4px; color: var(--cth-muted); font-size: 12px; text-align: right; }
49
+ .cth .cth-tools { display: flex; flex-direction: column; gap: 6px; }
50
+ .cth .cth-tool {
51
+ display: flex; align-items: center; gap: 9px; padding: 9px 11px; border-radius: 10px; background: var(--cth-fill);
52
+ font-size: 13px; font-weight: 550; cursor: pointer;
53
+ }
54
+ .cth .cth-tool:hover { background: var(--cth-fill-2); }
55
+ .cth .cth-tool-more { display: block; margin-left: auto; color: var(--cth-muted); font-size: 12px; font-weight: 400; }
56
+ .cth :is(.cth-tool, .cth-send, .cth-plain)[disabled] { opacity: 0.5; cursor: default; }
57
+ .cth .cth-chip {
58
+ display: flex; align-items: center; gap: 6px; padding: 5px 5px 5px 11px; border-radius: 10px;
59
+ box-shadow: inset 0 0 0 1px var(--cth-line); font-size: 12.5px;
60
+ }
61
+ .cth .cth-chip-text { display: block; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
62
+ .cth .cth-chip-what { font-weight: 600; }
63
+ .cth .cth-thumbs { display: flex; flex-wrap: wrap; gap: 8px; }
64
+ .cth .cth-thumb {
65
+ position: relative; display: block; width: 104px; height: 68px; border-radius: 10px; overflow: hidden;
66
+ background: var(--cth-fill); box-shadow: 0 0 0 1px var(--cth-line);
67
+ }
68
+ .cth .cth-thumb-img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top left; }
69
+ .cth .cth-thumb-x {
70
+ position: absolute; top: 4px; right: 4px; display: inline-flex; align-items: center; justify-content: center;
71
+ width: 22px; height: 22px; border-radius: 50%; background: rgba(20, 20, 22, 0.74); color: #ffffff; cursor: pointer;
72
+ }
73
+ .cth .cth-thumb-x .cth-icon, .cth .cth-chip .cth-icon { width: 12px; height: 12px; stroke-width: 2; }
74
+ .cth .cth-alert { display: block; color: var(--cth-danger); font-size: 12.5px; font-weight: 550; }
75
+ .cth .cth-hint { display: block; color: var(--cth-muted); font-size: 12.5px; }
76
+ .cth .cth-foot { display: flex; align-items: center; gap: 10px; margin-top: 2px; }
77
+ .cth .cth-plain { display: inline-block; color: var(--cth-link); font-size: 13px; font-weight: 550; cursor: pointer; }
78
+ .cth .cth-plain:hover { text-decoration: underline; }
79
+ .cth .cth-send {
80
+ display: inline-flex; align-items: center; justify-content: center; min-width: 84px; height: 36px; margin-left: auto;
81
+ padding: 0 16px; border-radius: 999px; background: var(--cth-accent); color: var(--cth-accent-ink);
82
+ font-size: 13px; font-weight: 650; cursor: pointer;
83
+ }
84
+ .cth .cth-sent { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 6px 0 2px; }
85
+ .cth .cth-sent-mark {
86
+ display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%;
87
+ background: var(--cth-ok); color: #ffffff;
88
+ }
89
+ .cth .cth-sent-mark .cth-icon { stroke-width: 2.2; }
90
+ .cth .cth-sent-title { display: block; font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
91
+ .cth .cth-sent-actions { display: flex; align-items: center; gap: 16px; margin-top: 4px; }
92
+ .cth .cth-list { display: flex; flex-direction: column; }
93
+ .cth .cth-row { display: block; border-top: 1px solid var(--cth-line); }
94
+ .cth .cth-row:first-child { border-top: 0; }
95
+ .cth .cth-row-main { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 2px 10px; padding: 10px 4px; border-radius: 8px; }
96
+ .cth a.cth-row-main { cursor: pointer; }
97
+ .cth a.cth-row-main:hover { background: var(--cth-fill); }
98
+ .cth .cth-row-summary { display: block; grid-column: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 600; }
99
+ .cth .cth-row-meta { display: block; grid-column: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--cth-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
100
+ .cth .cth-row-note { display: block; grid-column: 1 / -1; margin-top: 4px; color: var(--cth-muted); font-size: 12px; line-height: 1.4; }
101
+ .cth .cth-status {
102
+ display: inline-block; grid-column: 2; grid-row: 1 / span 2; padding: 3px 9px; border-radius: 999px;
103
+ background: var(--cth-received-bg); color: var(--cth-received-ink); font-size: 11.5px; font-weight: 650; white-space: nowrap;
104
+ }
105
+ .cth .cth-status[data-status="in_progress"] { background: var(--cth-progress-bg); color: var(--cth-progress-ink); }
106
+ .cth .cth-status[data-status="resolved"] { background: var(--cth-resolved-bg); color: var(--cth-resolved-ink); }
107
+ .cth .cth-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 22px 0 14px; color: var(--cth-muted); font-size: 13px; text-align: center; }
108
+ .cth .cth-live { position: absolute; display: block; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
109
+ @media (max-width: 480px) {
110
+ .cth .cth-panel, .cth[data-position] .cth-panel {
111
+ left: 0; right: 0; bottom: var(--cth-sheet-lift, 0px); width: auto; max-width: none;
112
+ max-height: calc(100vh - 48px - var(--cth-sheet-lift, 0px)); max-height: calc(100dvh - 48px - var(--cth-sheet-lift, 0px));
113
+ padding-bottom: env(safe-area-inset-bottom, 0px); border-radius: 18px 18px 0 0;
114
+ }
115
+ .cth .cth-text { font-size: 16px; }
116
+ }
117
+ @media (prefers-reduced-motion: no-preference) {
118
+ .cth .cth-panel { animation: cth-in 0.24s cubic-bezier(0.2, 0.8, 0.2, 1); transform-origin: bottom right; }
119
+ .cth[data-position="bottom-left"] .cth-panel { transform-origin: bottom left; }
120
+ .cth :is(.cth-tool, .cth-tab, .cth-x, .cth-send) { transition: background-color 0.16s ease, color 0.16s ease, opacity 0.16s ease; }
121
+ @keyframes cth-in { from { opacity: 0; transform: translateY(4px) scale(0.98); } }
122
+ }
123
+ `;