visual-comments 0.1.0 → 0.2.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 (55) hide show
  1. package/README.md +19 -2
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +477 -0
  4. package/dist/core/presence.d.ts +13 -0
  5. package/dist/core/presence.js +25 -0
  6. package/dist/core/threads.svelte.d.ts +36 -2
  7. package/dist/core/threads.svelte.js +120 -3
  8. package/dist/index.d.ts +1 -1
  9. package/dist/server/env.d.ts +78 -0
  10. package/dist/server/env.js +94 -0
  11. package/dist/server/index.d.ts +1 -0
  12. package/dist/server/index.js +10 -3
  13. package/dist/server/variables.d.ts +28 -0
  14. package/dist/server/variables.js +73 -0
  15. package/dist/shared/protocol.d.ts +4 -0
  16. package/dist/shared/protocol.js +4 -0
  17. package/dist/shared/types.d.ts +64 -0
  18. package/dist/sveltekit/CommentTools.svelte +98 -0
  19. package/dist/sveltekit/CommentTools.svelte.d.ts +6 -0
  20. package/dist/sveltekit/Comments.svelte +104 -0
  21. package/dist/sveltekit/Comments.svelte.d.ts +25 -0
  22. package/dist/sveltekit/Toolbar.svelte +64 -0
  23. package/dist/sveltekit/Toolbar.svelte.d.ts +13 -0
  24. package/dist/sveltekit/ToolbarButton.svelte +66 -0
  25. package/dist/sveltekit/ToolbarButton.svelte.d.ts +12 -0
  26. package/dist/sveltekit/context.d.ts +16 -0
  27. package/dist/sveltekit/context.js +9 -0
  28. package/dist/sveltekit/index.d.ts +6 -0
  29. package/dist/sveltekit/index.js +6 -0
  30. package/dist/ui/Account.svelte +45 -7
  31. package/dist/ui/App.svelte +12 -3
  32. package/dist/ui/Avatar.svelte +28 -6
  33. package/dist/ui/Avatar.svelte.d.ts +4 -5
  34. package/dist/ui/Connect.svelte +128 -0
  35. package/dist/ui/Connect.svelte.d.ts +6 -0
  36. package/dist/ui/Menu.svelte +5 -1
  37. package/dist/ui/Menu.svelte.d.ts +2 -0
  38. package/dist/ui/Overlay.svelte +15 -1
  39. package/dist/ui/Panel.svelte +141 -127
  40. package/dist/ui/Pin.svelte +22 -2
  41. package/dist/ui/Pin.svelte.d.ts +2 -0
  42. package/dist/ui/Presence.svelte +43 -0
  43. package/dist/ui/Presence.svelte.d.ts +8 -0
  44. package/dist/ui/ThreadCard.svelte +26 -2
  45. package/dist/ui/generated/styles.css +1 -1
  46. package/dist/ui/generated/styles.js +2 -2
  47. package/dist/ui/styles.css +21 -0
  48. package/dist/ui/text.d.ts +3 -1
  49. package/dist/ui/text.js +4 -0
  50. package/dist/visual-comments.svelte.d.ts +53 -4
  51. package/dist/visual-comments.svelte.js +154 -29
  52. package/dist/vite/index.d.ts +20 -0
  53. package/dist/vite/index.js +78 -0
  54. package/dist-widget/visual-comments.js +17 -10
  55. package/package.json +88 -66
@@ -0,0 +1,98 @@
1
+ <!--
2
+ The comments' part of the toolbar, while they are on: the comment button and the account (who
3
+ is here, and the viewer's avatar or Sign in), after a divider when the app's tools come first.
4
+ Left out of builds without the comments (visual-comments/vite).
5
+ -->
6
+ <script lang="ts">
7
+ import { page } from '$app/state';
8
+ import { useComments } from './context.js';
9
+ import ToolbarButton from './ToolbarButton.svelte';
10
+
11
+ let { divider = false }: { divider?: boolean } = $props();
12
+
13
+ const comments = useComments();
14
+ const shortcut = $derived(comments?.current?.options.shortcut ?? 'c');
15
+ const viewer = $derived(comments?.viewer);
16
+ const initial = $derived(Array.from(viewer?.name.trim() || '?')[0].toLocaleUpperCase());
17
+ const signIn = $derived(`${comments?.auth.replace(/\/$/, '')}/login?${new URLSearchParams({ returnTo: `${page.url.pathname}${page.url.search}` })}`);
18
+ </script>
19
+
20
+ {#if comments?.enabled}
21
+ {#if divider}<span class="vc-toolbar-divider" aria-hidden="true"></span>{/if}
22
+ <!-- The comments find this button by `data-vc-toggle`: clicks turn comment mode on and off, and they set `aria-pressed` and `data-vc-unread`. -->
23
+ <ToolbarButton
24
+ data-vc-toggle
25
+ label={shortcut === false ? 'Comments' : `Comments (${shortcut.toUpperCase()})`}
26
+ disabled={!comments.current}
27
+ >
28
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
29
+ <path d="M12 3.5a8.5 8.5 0 1 1 0 17H3.5V12A8.5 8.5 0 0 1 12 3.5z" />
30
+ </svg>
31
+ </ToolbarButton>
32
+ <!-- The comments put the account button here once they have started; until then, it shows who the server said is signed in. -->
33
+ <span class="vc-toolbar-account" data-vc-account>
34
+ {#if !comments.current && viewer}
35
+ <span class="vc-toolbar-tool vc-toolbar-me" title={viewer.name}>
36
+ <span class="vc-toolbar-avatar" aria-hidden="true">
37
+ {#if viewer.picture}<img src={viewer.picture} alt="" referrerpolicy="no-referrer" />{:else}{initial}{/if}
38
+ </span>
39
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
40
+ <path d="m6 9 6 6 6-6" />
41
+ </svg>
42
+ </span>
43
+ {:else if !comments.current && viewer === null}
44
+ <a class="vc-toolbar-tool vc-toolbar-sign-in" href={signIn}>Sign in</a>
45
+ {/if}
46
+ </span>
47
+ {/if}
48
+
49
+ <style>
50
+ .vc-toolbar-divider {
51
+ width: 1px;
52
+ height: 1.25rem;
53
+ margin: 0 0.25rem;
54
+ background: currentColor;
55
+ opacity: 0.25;
56
+ }
57
+ /* The account draws in its own shadow root and takes the toolbar's text colour. */
58
+ .vc-toolbar-account {
59
+ display: inline-flex;
60
+ }
61
+ /* Until then, the same sizes as the account button. */
62
+ .vc-toolbar-tool {
63
+ display: inline-flex;
64
+ height: 2rem;
65
+ align-items: center;
66
+ gap: 0.375rem;
67
+ border-radius: 9999px;
68
+ color: inherit;
69
+ text-decoration: none;
70
+ }
71
+ .vc-toolbar-me {
72
+ padding: 0 0.5rem 0 0.125rem;
73
+ }
74
+ .vc-toolbar-sign-in {
75
+ padding: 0 0.875rem;
76
+ font-weight: 500;
77
+ }
78
+ .vc-toolbar-sign-in:hover {
79
+ background: color-mix(in srgb, currentColor 14%, transparent);
80
+ }
81
+ .vc-toolbar-avatar {
82
+ display: grid;
83
+ width: 28px;
84
+ height: 28px;
85
+ place-items: center;
86
+ overflow: hidden;
87
+ border-radius: 9999px;
88
+ background: var(--vc-avatar, #009aaa);
89
+ color: #fff;
90
+ font-size: 14px;
91
+ line-height: 1;
92
+ }
93
+ .vc-toolbar-avatar img {
94
+ width: 100%;
95
+ height: 100%;
96
+ object-fit: cover;
97
+ }
98
+ </style>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ divider?: boolean;
3
+ };
4
+ declare const CommentTools: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type CommentTools = ReturnType<typeof CommentTools>;
6
+ export default CommentTools;
@@ -0,0 +1,104 @@
1
+ <!--
2
+ Adds comments to a SvelteKit app. Put it in the root layout, around the
3
+ pages, with the config the server's `commentsFromEnv` gives the layout's
4
+ `load`. With no config, comments are off: it renders only the pages, and the
5
+ browser never loads the comments' code.
6
+ -->
7
+ <script lang="ts" module>
8
+ import type { Snippet } from 'svelte';
9
+ import type { CommentsClientConfig } from '../server/env.js';
10
+ import type { VisualComments, VisualCommentsOptions } from '../visual-comments.svelte.js';
11
+
12
+ export interface CommentsProps extends Omit<VisualCommentsOptions, 'endpoint' | 'auth' | 'page'> {
13
+ /** Where the comments and sign-in are, from `commentsFromEnv(...).client`. Null or undefined: comments are off. */
14
+ config: CommentsClientConfig | null | undefined;
15
+ /**
16
+ * Which threads a page shows. `path` (default): each address's own, by
17
+ * origin and path. `route`: every address of a route shares them, so
18
+ * `/projects/1` and `/projects/2` show the threads of `/projects/[id]`.
19
+ * A string: one set for every page. A function: a key for each address.
20
+ */
21
+ page?: 'path' | 'route' | (string & {}) | ((url: URL) => string);
22
+ /** Signs in and out another way than the sign-in routes of `config.auth`. */
23
+ auth?: { signIn(): void; signOut(): void };
24
+ /** The comments while they run, to bind to. */
25
+ comments?: VisualComments;
26
+ children?: Snippet;
27
+ }
28
+ </script>
29
+
30
+ <script lang="ts">
31
+ import { untrack } from 'svelte';
32
+ import { goto } from '$app/navigation';
33
+ import { page as current } from '$app/state';
34
+ import { setAppComments } from './context.js';
35
+
36
+ let { config, page = 'path', auth, comments = $bindable(), children, ...rest }: CommentsProps = $props();
37
+
38
+ // Started again only when where the comments are changes, not whenever `load` runs again.
39
+ const endpoint = $derived(config?.endpoint);
40
+ const authPath = $derived(config?.auth);
41
+
42
+ setAppComments({
43
+ get enabled() { return Boolean(endpoint); },
44
+ get current() { return comments; },
45
+ get viewer() { return config?.viewer; },
46
+ get auth() { return authPath ?? '/auth'; }
47
+ });
48
+
49
+ /** The page key of an address: its origin and path, without a trailing slash. */
50
+ function pathKey(url: URL) {
51
+ const path = url.pathname.length > 1 ? url.pathname.replace(/\/+$/, '') : url.pathname;
52
+ return `${url.origin}${path}`;
53
+ }
54
+
55
+ function pageOption(option: NonNullable<CommentsProps['page']>): VisualCommentsOptions['page'] {
56
+ if (option === 'path') return undefined;
57
+ // Read when the comments measure the page, by when the router has updated `page`.
58
+ if (option === 'route') return (url) => (current.route.id ? `${url.origin}${current.route.id}` : pathKey(url));
59
+ return option;
60
+ }
61
+
62
+ /** Goes to a thread on another page of the app with the router, so the comments stay open. */
63
+ function navigate(link: string) {
64
+ const url = new URL(link, location.href);
65
+ if (url.origin !== location.origin) return location.assign(link);
66
+ // The thread is already open; its link's `#vc=<id>` is only for addresses opened anew.
67
+ const hash = url.hash.slice(1).split('&').filter((part) => part && !part.startsWith('vc=')).join('&');
68
+ void goto(`${url.pathname}${url.search}${hash ? `#${hash}` : ''}`).then(() => comments?.relayout());
69
+ }
70
+
71
+ const options = $derived<VisualCommentsOptions>({
72
+ navigate,
73
+ ...rest,
74
+ page: pageOption(page),
75
+ ...(auth ? { auth } : {})
76
+ });
77
+
78
+ $effect(() => {
79
+ if (!endpoint) return;
80
+ const start = { endpoint, auth: authPath ?? '/auth' };
81
+ let stopped = false;
82
+ let instance: VisualComments | undefined;
83
+ // Loaded only when comments are on, so an app without them never downloads their code.
84
+ void import('../index.js').then(({ createVisualComments }) => {
85
+ if (stopped) return;
86
+ // Who the page was loaded for, so the account shows without waiting for the server.
87
+ instance = createVisualComments({ ...untrack(() => options), ...start, viewer: untrack(() => config?.viewer) });
88
+ comments = instance;
89
+ });
90
+ return () => {
91
+ stopped = true;
92
+ instance?.destroy();
93
+ if (untrack(() => comments) === instance) comments = undefined;
94
+ };
95
+ });
96
+
97
+ // Options the app changes later, e.g. a `show` that depends on its state.
98
+ $effect(() => {
99
+ const next = options;
100
+ untrack(() => comments)?.configure(next);
101
+ });
102
+ </script>
103
+
104
+ {@render children?.()}
@@ -0,0 +1,25 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { CommentsClientConfig } from '../server/env.js';
3
+ import type { VisualComments, VisualCommentsOptions } from '../visual-comments.svelte.js';
4
+ export interface CommentsProps extends Omit<VisualCommentsOptions, 'endpoint' | 'auth' | 'page'> {
5
+ /** Where the comments and sign-in are, from `commentsFromEnv(...).client`. Null or undefined: comments are off. */
6
+ config: CommentsClientConfig | null | undefined;
7
+ /**
8
+ * Which threads a page shows. `path` (default): each address's own, by
9
+ * origin and path. `route`: every address of a route shares them, so
10
+ * `/projects/1` and `/projects/2` show the threads of `/projects/[id]`.
11
+ * A string: one set for every page. A function: a key for each address.
12
+ */
13
+ page?: 'path' | 'route' | (string & {}) | ((url: URL) => string);
14
+ /** Signs in and out another way than the sign-in routes of `config.auth`. */
15
+ auth?: {
16
+ signIn(): void;
17
+ signOut(): void;
18
+ };
19
+ /** The comments while they run, to bind to. */
20
+ comments?: VisualComments;
21
+ children?: Snippet;
22
+ }
23
+ declare const Comments: import("svelte").Component<CommentsProps, {}, "comments">;
24
+ type Comments = ReturnType<typeof Comments>;
25
+ export default Comments;
@@ -0,0 +1,64 @@
1
+ <!--
2
+ A toolbar in the style of Figma's and Vercel's: a pill of dark glass,
3
+ centred at the bottom of the window (or the top, or wherever the app puts it
4
+ with `position="inline"`). The app's own tools go first, as its content,
5
+ e.g. `ToolbarButton`s; then, while comments are on, the comment button and
6
+ the account (who is here, and the viewer's avatar or Sign in). It replaces
7
+ the comments' own floating toolbar. With comments off and no tools of its
8
+ own, it is not shown.
9
+ -->
10
+ <script lang="ts">
11
+ import type { Snippet } from 'svelte';
12
+ import CommentTools from './CommentTools.svelte';
13
+ import { useComments } from './context.js';
14
+
15
+ let { position = 'bottom', label = 'Toolbar', class: className, children }: {
16
+ /** `bottom` or `top` of the window, centred; `inline` where the app places it. Default `bottom`. */
17
+ position?: 'bottom' | 'top' | 'inline';
18
+ /** The toolbar's name for screen readers. */
19
+ label?: string;
20
+ class?: string;
21
+ /** The app's own tools. */
22
+ children?: Snippet;
23
+ } = $props();
24
+
25
+ const comments = useComments();
26
+ </script>
27
+
28
+ {#if comments?.enabled || children}
29
+ <!-- Never takes comments itself, even over a surface. -->
30
+ <div role="toolbar" aria-label={label} data-vc-ignore class={['vc-toolbar', position, className]}>
31
+ {@render children?.()}
32
+ <CommentTools divider={Boolean(children)} />
33
+ </div>
34
+ {/if}
35
+
36
+ <style>
37
+ .vc-toolbar {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ gap: 0.125rem;
41
+ border-radius: 9999px;
42
+ padding: 0.25rem;
43
+ background: var(--vc-island, rgb(50 50 50 / 0.72));
44
+ color: var(--vc-island-ink, #fff);
45
+ font: 13px var(--vc-font, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif);
46
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.2);
47
+ backdrop-filter: blur(5px);
48
+ -webkit-backdrop-filter: blur(5px);
49
+ }
50
+ .vc-toolbar.bottom,
51
+ .vc-toolbar.top {
52
+ position: fixed;
53
+ left: 50%;
54
+ translate: -50% 0;
55
+ /* Under the comments' own layer, so their cards and menus open over it. */
56
+ z-index: 2147483000;
57
+ }
58
+ .vc-toolbar.bottom {
59
+ bottom: 1rem;
60
+ }
61
+ .vc-toolbar.top {
62
+ top: 1rem;
63
+ }
64
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** `bottom` or `top` of the window, centred; `inline` where the app places it. Default `bottom`. */
4
+ position?: 'bottom' | 'top' | 'inline';
5
+ /** The toolbar's name for screen readers. */
6
+ label?: string;
7
+ class?: string;
8
+ /** The app's own tools. */
9
+ children?: Snippet;
10
+ };
11
+ declare const Toolbar: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type Toolbar = ReturnType<typeof Toolbar>;
13
+ export default Toolbar;
@@ -0,0 +1,66 @@
1
+ <!--
2
+ A tool of the comments' toolbar, for the app's own tools beside the
3
+ comment button: round, light on the toolbar's dark glass, and filled while
4
+ `pressed`. Give it an icon as its content and a `label`.
5
+ -->
6
+ <script lang="ts">
7
+ import type { Snippet } from 'svelte';
8
+ import type { HTMLButtonAttributes } from 'svelte/elements';
9
+
10
+ let { label, pressed, children, ...rest }: HTMLButtonAttributes & {
11
+ /** What the tool does, for screen readers and as its tooltip. */
12
+ label: string;
13
+ /** For a tool that is on or off. */
14
+ pressed?: boolean;
15
+ children: Snippet;
16
+ } = $props();
17
+ </script>
18
+
19
+ <button type="button" class="vc-toolbar-button" aria-label={label} title={label} aria-pressed={pressed} {...rest}>
20
+ {@render children()}
21
+ </button>
22
+
23
+ <style>
24
+ .vc-toolbar-button {
25
+ position: relative;
26
+ display: inline-flex;
27
+ height: 2rem;
28
+ min-width: 2rem;
29
+ align-items: center;
30
+ justify-content: center;
31
+ gap: 0.375rem;
32
+ border: 0;
33
+ border-radius: 9999px;
34
+ padding: 0 0.5rem;
35
+ background: transparent;
36
+ color: inherit;
37
+ font: inherit;
38
+ cursor: pointer;
39
+ outline: none;
40
+ }
41
+ .vc-toolbar-button:hover {
42
+ background: var(--vc-island-hover, rgb(255 255 255 / 0.14));
43
+ }
44
+ .vc-toolbar-button[aria-pressed='true'] {
45
+ background: var(--vc-island-ink, #fff);
46
+ color: var(--vc-ink, #292524);
47
+ }
48
+ .vc-toolbar-button:focus-visible {
49
+ box-shadow: 0 0 0 2px rgb(255 255 255 / 0.6);
50
+ }
51
+ .vc-toolbar-button:disabled {
52
+ opacity: 0.5;
53
+ cursor: default;
54
+ }
55
+ /* Something is unread: a dot, as on the comments' own toolbar. */
56
+ .vc-toolbar-button[data-vc-unread]::after {
57
+ content: '';
58
+ position: absolute;
59
+ top: 0.25rem;
60
+ right: 0.25rem;
61
+ width: 0.5rem;
62
+ height: 0.5rem;
63
+ border-radius: 9999px;
64
+ background: var(--vc-comment, #0d99ff);
65
+ }
66
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ type $$ComponentProps = HTMLButtonAttributes & {
4
+ /** What the tool does, for screen readers and as its tooltip. */
5
+ label: string;
6
+ /** For a tool that is on or off. */
7
+ pressed?: boolean;
8
+ children: Snippet;
9
+ };
10
+ declare const ToolbarButton: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type ToolbarButton = ReturnType<typeof ToolbarButton>;
12
+ export default ToolbarButton;
@@ -0,0 +1,16 @@
1
+ import type { Viewer } from '../shared/types.js';
2
+ import type { VisualComments } from '../visual-comments.svelte.js';
3
+ /** The comments of the app, as `<Comments>` provides them. Both fields are reactive. */
4
+ export interface AppComments {
5
+ /** Whether comments are on for this deployment: `<Comments config>` is set. */
6
+ readonly enabled: boolean;
7
+ /** The comments, once they have started in the browser; undefined before, during server rendering, and while off. */
8
+ readonly current: VisualComments | undefined;
9
+ /** Who is signed in as the page loaded (`comments.load` on the server); undefined when it did not say. */
10
+ readonly viewer: Viewer | null | undefined;
11
+ /** Where the sign-in routes are, e.g. `/auth`. */
12
+ readonly auth: string;
13
+ }
14
+ export declare function setAppComments(comments: AppComments): void;
15
+ /** The comments of the app, inside `<Comments>`; undefined outside it. */
16
+ export declare function useComments(): AppComments | undefined;
@@ -0,0 +1,9 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const KEY = Symbol('visual-comments/sveltekit');
3
+ export function setAppComments(comments) {
4
+ setContext(KEY, comments);
5
+ }
6
+ /** The comments of the app, inside `<Comments>`; undefined outside it. */
7
+ export function useComments() {
8
+ return getContext(KEY);
9
+ }
@@ -0,0 +1,6 @@
1
+ export { default as Comments, type CommentsProps } from './Comments.svelte';
2
+ export { default as Toolbar } from './Toolbar.svelte';
3
+ export { default as ToolbarButton } from './ToolbarButton.svelte';
4
+ export { useComments, type AppComments } from './context.js';
5
+ export type { CommentsClientConfig } from '../server/env.js';
6
+ export type { VisualComments, VisualCommentsOptions } from '../visual-comments.svelte.js';
@@ -0,0 +1,6 @@
1
+ // `visual-comments/sveltekit`: comments in a SvelteKit app. The server side,
2
+ // set up from environment variables, is `commentsFromEnv` of `visual-comments/server`.
3
+ export { default as Comments } from './Comments.svelte';
4
+ export { default as Toolbar } from './Toolbar.svelte';
5
+ export { default as ToolbarButton } from './ToolbarButton.svelte';
6
+ export { useComments } from './context.js';
@@ -1,28 +1,42 @@
1
1
  <svelte:options css="injected" />
2
2
 
3
3
  <!--
4
- The account button: the viewer's avatar with a menu to sign out, or Sign in.
5
- It sits in the floating toolbar, or in a host's own toolbar where the host
6
- puts a `data-vc-account` element; there it takes the host's text colour.
4
+ The account button: the viewer's avatar with a menu to sign out and to see
5
+ the space's agents, or Sign in; beside it, who else is here. It sits in the
6
+ floating toolbar, or in a host's own toolbar where the host puts a
7
+ `data-vc-account` element; there it takes the host's text colour.
7
8
  -->
8
9
  <script lang="ts">
9
10
  import type { VisualComments } from '../visual-comments.svelte.js';
11
+ import type { Agent } from '../shared/types.js';
10
12
  import Avatar from './Avatar.svelte';
11
13
  import { setComments } from './context.js';
12
14
  import Icon from './Icon.svelte';
13
15
  import Menu from './Menu.svelte';
14
16
  import MenuItem from './MenuItem.svelte';
17
+ import Presence from './Presence.svelte';
15
18
 
16
19
  let { vc, slot = false }: { vc: VisualComments; slot?: boolean } = $props();
17
20
  // svelte-ignore state_referenced_locally
18
21
  if (slot) setComments(vc);
19
22
 
20
23
  const tool = $derived(slot ? 'vc-slot-tool' : 'vc-island-tool');
24
+ const signedIn = $derived(Boolean(vc.viewer));
25
+
26
+ // Listed ahead, so the menu opens at its full size.
27
+ $effect(() => {
28
+ if (signedIn) void vc.store.loadAgents();
29
+ });
30
+
31
+ function remove(agent: Agent) {
32
+ if (confirm(`Remove ${agent.name}? It will no longer see or write comments here.`)) void vc.store.removeAgent(agent.id);
33
+ }
21
34
  </script>
22
35
 
23
- <span class={['vc-ui inline-flex', slot && 'vc-slot']}>
36
+ <span class={['vc-ui inline-flex items-center', slot && 'vc-slot']}>
24
37
  {#if vc.viewer}
25
- <Menu label={`Account: ${vc.viewer.name}`} align="end" class="{tool} pr-2 pl-0.5">
38
+ <Presence {vc} />
39
+ <Menu label={`Account: ${vc.viewer.name}`} align="end" class="{tool} pr-2 pl-0.5" onopen={() => vc.store.loadAgents()}>
26
40
  {#snippet trigger()}
27
41
  {#if vc.viewer}<Avatar user={vc.viewer} size={28} />{/if}
28
42
  <Icon name="chevron-down" size={14} />
@@ -33,12 +47,36 @@
33
47
  {#if vc.viewer?.email}<p class="mt-0.5 truncate text-xs text-vc-muted">{vc.viewer.email}</p>{/if}
34
48
  {#if vc.store.spaceName}<p class="mt-0.5 truncate text-xs text-vc-muted">Comments in {vc.store.spaceName}</p>{/if}
35
49
  </div>
50
+ {#if vc.store.agents}
51
+ <div class="max-w-72 border-b border-vc-line py-1.5">
52
+ <p class="px-3 pb-1 text-xs text-vc-muted">Agents</p>
53
+ {#each vc.store.agents as agent (agent.id)}
54
+ <div class="flex items-center gap-2 px-3 py-1">
55
+ <Avatar user={{ sub: `agent:${agent.id}`, name: agent.name, agent: { addedBy: agent.addedBy } }} size={22} />
56
+ <div class="min-w-0 flex-1">
57
+ <p class="truncate">{agent.name}</p>
58
+ <p class="truncate text-xs text-vc-muted">Added by {agent.addedBy.name}</p>
59
+ </div>
60
+ <button
61
+ type="button"
62
+ class="shrink-0 rounded-[2px] px-1.5 py-0.5 text-xs text-vc-muted outline-none hover:bg-vc-wash hover:text-vc-ink focus-visible:bg-vc-wash"
63
+ aria-label="Remove {agent.name}"
64
+ onclick={() => remove(agent)}
65
+ >Remove</button>
66
+ </div>
67
+ {:else}
68
+ <p class="px-3 pb-1 text-xs text-vc-muted">
69
+ None yet. An agent asks to join with <code class="font-mono">npx visual-comments connect</code>.
70
+ </p>
71
+ {/each}
72
+ </div>
73
+ {/if}
36
74
  <MenuItem onselect={() => { close(); vc.signOut(); }}>Sign out</MenuItem>
37
75
  {/snippet}
38
76
  </Menu>
39
77
  {:else}
40
- <button type="button" part="button" class="{tool} px-3.5 font-medium" disabled={vc.loading} onclick={() => vc.signIn()}>
41
- {vc.loading ? 'Connecting…' : 'Sign in'}
78
+ <button type="button" part="button" class="{tool} px-3.5 font-medium" disabled={!vc.store.viewerKnown} onclick={() => vc.signIn()}>
79
+ {vc.store.viewerKnown ? 'Sign in' : 'Connecting…'}
42
80
  </button>
43
81
  {/if}
44
82
  </span>
@@ -1,12 +1,13 @@
1
1
  <!--
2
2
  Everything the comments draw over the page, in one shadow root: layers that
3
3
  catch clicks over frames in comment mode, pins and cards, the list of
4
- threads and the floating toolbar.
4
+ threads, the floating toolbar and the dialog that lets an agent join.
5
5
  -->
6
6
  <script lang="ts">
7
7
  import type { VisualComments } from '../visual-comments.svelte.js';
8
8
  import type { Surface } from '../core/surfaces.js';
9
9
  import { setComments } from './context.js';
10
+ import Connect from './Connect.svelte';
10
11
  import Overlay from './Overlay.svelte';
11
12
  import Panel from './Panel.svelte';
12
13
  import Toolbar from './Toolbar.svelte';
@@ -55,8 +56,10 @@
55
56
 
56
57
  {#if vc.active}
57
58
  <Panel
58
- class={panel ? 'absolute z-20' : 'absolute top-3 right-3 bottom-3 z-20 w-80'}
59
- style={panel ? `left:${panel.left}px;top:${panel.top}px;width:${panel.right - panel.left}px;height:${panel.bottom - panel.top}px` : ''}
59
+ class={panel ? 'absolute z-20' : vc.panelCollapsed ? 'absolute top-3 right-3 z-20 w-40' : 'absolute top-3 right-3 bottom-3 z-20 w-80'}
60
+ style={panel ? vc.panelCollapsed
61
+ ? `left:${Math.max(panel.left, panel.right - 160)}px;top:${panel.top}px;width:${Math.min(160, panel.right - panel.left)}px;height:46px`
62
+ : `left:${panel.left}px;top:${panel.top}px;width:${panel.right - panel.left}px;height:${panel.bottom - panel.top}px` : ''}
60
63
  onselect={(thread) => vc.focus(thread)}
61
64
  onclose={() => vc.toggle(false)}
62
65
  />
@@ -65,4 +68,10 @@
65
68
  {#if vc.showToolbar}
66
69
  <Toolbar />
67
70
  {/if}
71
+
72
+ {#if vc.connecting}
73
+ {#key vc.connecting}
74
+ <Connect id={vc.connecting} />
75
+ {/key}
76
+ {/if}
68
77
  </div>
@@ -1,4 +1,7 @@
1
- <!-- A person's profile picture, or their initial on a colour of their own. -->
1
+ <!--
2
+ A person's profile picture, or their initial on a colour of their own.
3
+ An agent's has a badge, and says who added it when hovered.
4
+ -->
2
5
  <script lang="ts" module>
3
6
  // Others get a stable colour from their id; the viewer keeps the account colour.
4
7
  const COLORS = ['#7b61ff', '#f24822', '#14ae5c', '#ffa629', '#0d99ff', '#e5487d', '#667799'];
@@ -11,11 +14,15 @@
11
14
  </script>
12
15
 
13
16
  <script lang="ts">
17
+ import type { Author } from '../shared/types.js';
14
18
  import { useComments } from './context.js';
19
+ import { authorLabel } from './text.js';
15
20
 
16
- let { user, size = 24, class: className = '' }: {
17
- user: { sub: string; name: string; picture?: string };
21
+ let { user, size = 24, title, class: className = '' }: {
22
+ user: Author;
18
23
  size?: number;
24
+ /** Default: an agent's name and who added it. */
25
+ title?: string;
19
26
  class?: string;
20
27
  } = $props();
21
28
 
@@ -23,20 +30,35 @@
23
30
  let failed = $state<string>();
24
31
  const initial = $derived(Array.from(user.name.trim() || '?')[0].toLocaleUpperCase());
25
32
  const self = $derived(vc?.viewer?.sub === user.sub);
33
+ const badge = $derived(Math.max(10, Math.round(size * 0.45)));
26
34
  </script>
27
35
 
28
36
  <span
29
- class={['flex shrink-0 items-center justify-center overflow-hidden rounded-full leading-none text-white select-none', className]}
37
+ class={['grid shrink-0 place-items-center rounded-full leading-none text-white select-none', className]}
30
38
  style:width="{size}px"
31
39
  style:height="{size}px"
32
40
  style:font-size="{Math.round(size * 0.5)}px"
33
41
  style:background={self ? 'var(--color-vc-avatar)' : colorFor(user.sub)}
42
+ title={title ?? (user.agent ? authorLabel(user) : undefined)}
34
43
  aria-hidden="true"
35
44
  >
36
45
  {#if user.picture && user.picture !== failed}
37
- <img src={user.picture} alt="" referrerpolicy="no-referrer" class="size-full object-cover" draggable="false"
46
+ <img src={user.picture} alt="" referrerpolicy="no-referrer" class="size-full rounded-full object-cover [grid-area:1/1]" draggable="false"
38
47
  onerror={() => { failed = user.picture; }} />
39
48
  {:else}
40
- {initial}
49
+ <span class="[grid-area:1/1]">{initial}</span>
50
+ {/if}
51
+ {#if user.agent}
52
+ <!-- Past the corner, like a status dot; nothing clips it. -->
53
+ <span
54
+ class="grid place-items-center self-end justify-self-end rounded-full bg-vc-agent text-white ring-[1.5px] ring-white [grid-area:1/1]"
55
+ style:width="{badge}px"
56
+ style:height="{badge}px"
57
+ style:margin="0 {-badge / 4}px {-badge / 4}px 0"
58
+ >
59
+ <svg width={badge - 3} height={badge - 3} viewBox="0 0 24 24" fill="currentColor">
60
+ <path d="M12 2c.8 5.6 4.4 9.2 10 10-5.6.8-9.2 4.4-10 10-.8-5.6-4.4-9.2-10-10 5.6-.8 9.2-4.4 10-10z" />
61
+ </svg>
62
+ </span>
41
63
  {/if}
42
64
  </span>
@@ -1,10 +1,9 @@
1
+ import type { Author } from '../shared/types.js';
1
2
  type $$ComponentProps = {
2
- user: {
3
- sub: string;
4
- name: string;
5
- picture?: string;
6
- };
3
+ user: Author;
7
4
  size?: number;
5
+ /** Default: an agent's name and who added it. */
6
+ title?: string;
8
7
  class?: string;
9
8
  };
10
9
  declare const Avatar: import("svelte").Component<$$ComponentProps, {}, "">;