@zooid/web 0.5.0 → 0.6.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 (40) hide show
  1. package/dist/assets/index-7P1i28a8.js +66 -0
  2. package/dist/assets/index-BeqmFlCW.css +1 -0
  3. package/dist/assets/json-editor-Cvlnnf1Q.css +1 -0
  4. package/dist/assets/json-editor-iJIPDKxB.js +84 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +11 -5
  7. package/src/App.svelte +678 -0
  8. package/src/app.css +178 -0
  9. package/src/lib/api.ts +78 -0
  10. package/src/lib/components/admin-dropdown.svelte +89 -0
  11. package/src/lib/components/auth-modal.svelte +114 -0
  12. package/src/lib/components/avatar.svelte +29 -0
  13. package/src/lib/components/channel-header.svelte +73 -0
  14. package/src/lib/components/create-channel-modal.svelte +137 -0
  15. package/src/lib/components/edit-channel-modal.svelte +234 -0
  16. package/src/lib/components/event-card.svelte +221 -0
  17. package/src/lib/components/event-feed.svelte +50 -0
  18. package/src/lib/components/homepage.svelte +86 -0
  19. package/src/lib/components/json-editor.svelte +57 -0
  20. package/src/lib/components/keys-and-tokens-page.svelte +216 -0
  21. package/src/lib/components/keys-modal.svelte +120 -0
  22. package/src/lib/components/message-bar.svelte +290 -0
  23. package/src/lib/components/mint-token-modal.svelte +141 -0
  24. package/src/lib/components/ref-link.svelte +33 -0
  25. package/src/lib/components/ref-side-sheet.svelte +105 -0
  26. package/src/lib/components/server-config-modal.svelte +141 -0
  27. package/src/lib/components/server-config-page.svelte +130 -0
  28. package/src/lib/components/sidebar.svelte +144 -0
  29. package/src/lib/components/status-bar.svelte +40 -0
  30. package/src/lib/pretty-json.test.ts +200 -0
  31. package/src/lib/pretty-json.ts +79 -0
  32. package/src/lib/time.ts +38 -0
  33. package/src/lib/zooid-uri.test.ts +102 -0
  34. package/src/lib/zooid-uri.ts +74 -0
  35. package/src/main.ts +7 -0
  36. package/src/vite-env.d.ts +2 -0
  37. package/dist/assets/index-C_9lWQjz.css +0 -1
  38. package/dist/assets/index-DWNlOxX1.js +0 -66
  39. package/dist/assets/json-editor-CPxgFho2.js +0 -84
  40. package/dist/assets/json-editor-DfH04Znl.css +0 -1
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import { Button } from '@ui/components/button/index';
3
+ import { Input } from '@ui/components/input/index';
4
+ import type { ZooidClient } from '@zooid/sdk';
5
+
6
+ let {
7
+ open,
8
+ client,
9
+ onClose,
10
+ }: {
11
+ open: boolean;
12
+ client: ZooidClient;
13
+ onClose: () => void;
14
+ } = $props();
15
+
16
+ let scopes = $state('pub:*, sub:*');
17
+ let sub = $state('');
18
+ let name = $state('');
19
+ let expiresIn = $state('');
20
+ let minting = $state(false);
21
+ let error = $state('');
22
+ let mintedToken = $state('');
23
+ let copied = $state(false);
24
+
25
+ $effect(() => {
26
+ if (open) {
27
+ error = '';
28
+ mintedToken = '';
29
+ copied = false;
30
+ }
31
+ });
32
+
33
+ async function handleMint(e: Event) {
34
+ e.preventDefault();
35
+ minting = true;
36
+ error = '';
37
+ mintedToken = '';
38
+ copied = false;
39
+
40
+ const scopeList = scopes.split(',').map((s) => s.trim()).filter(Boolean);
41
+ if (scopeList.length === 0) {
42
+ error = 'At least one scope is required';
43
+ minting = false;
44
+ return;
45
+ }
46
+
47
+ try {
48
+ const result = await client.mintToken({
49
+ scopes: scopeList,
50
+ sub: sub || undefined,
51
+ name: name || undefined,
52
+ expires_in: expiresIn || undefined,
53
+ });
54
+ mintedToken = result.token;
55
+ } catch (err) {
56
+ error = err instanceof Error ? err.message : 'Failed to mint token';
57
+ } finally {
58
+ minting = false;
59
+ }
60
+ }
61
+
62
+ async function copyToken() {
63
+ await navigator.clipboard.writeText(mintedToken);
64
+ copied = true;
65
+ setTimeout(() => { copied = false; }, 2000);
66
+ }
67
+
68
+ function handleBackdrop(e: MouseEvent) {
69
+ if (e.target === e.currentTarget) onClose();
70
+ }
71
+
72
+ function handleKeydown(e: KeyboardEvent) {
73
+ if (e.key === 'Escape') onClose();
74
+ }
75
+ </script>
76
+
77
+ {#if open}
78
+ <div
79
+ class="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
80
+ role="dialog"
81
+ aria-modal="true"
82
+ aria-label="Mint token"
83
+ tabindex="-1"
84
+ onclick={handleBackdrop}
85
+ onkeydown={handleKeydown}
86
+ >
87
+ <div class="bg-card border border-border rounded-lg shadow-lg w-full max-w-sm p-6">
88
+ <h2 class="font-semibold text-sm mb-1">Mint Token</h2>
89
+ <p class="text-xs text-muted-foreground mb-4">
90
+ Create a new JWT with custom scopes.
91
+ </p>
92
+
93
+ {#if mintedToken}
94
+ <div class="flex flex-col gap-3">
95
+ <div class="bg-secondary rounded-md p-3">
96
+ <div class="text-[10px] text-muted-foreground mb-1 uppercase tracking-wider">Token (shown once)</div>
97
+ <div class="text-xs font-mono break-all select-all max-h-24 overflow-y-auto">{mintedToken}</div>
98
+ </div>
99
+ <div class="flex gap-2">
100
+ <Button size="sm" class="flex-1" onclick={copyToken}>
101
+ {copied ? 'Copied!' : 'Copy'}
102
+ </Button>
103
+ <Button variant="outline" size="sm" class="flex-1" onclick={onClose}>Done</Button>
104
+ </div>
105
+ </div>
106
+ {:else}
107
+ <form onsubmit={handleMint} class="flex flex-col gap-3">
108
+ <label class="flex flex-col gap-1">
109
+ <span class="text-xs text-muted-foreground">Scopes (comma-separated)</span>
110
+ <Input bind:value={scopes} placeholder="admin, pub:my-channel, sub:*" />
111
+ <span class="text-[10px] text-muted-foreground/60">admin, pub:channel, sub:channel, pub:*, sub:*</span>
112
+ </label>
113
+ <label class="flex flex-col gap-1">
114
+ <span class="text-xs text-muted-foreground">Subject (optional)</span>
115
+ <Input bind:value={sub} placeholder="my-bot" />
116
+ </label>
117
+ <label class="flex flex-col gap-1">
118
+ <span class="text-xs text-muted-foreground">Display name (optional)</span>
119
+ <Input bind:value={name} placeholder="My Bot" />
120
+ </label>
121
+ <label class="flex flex-col gap-1">
122
+ <span class="text-xs text-muted-foreground">Expires in (optional)</span>
123
+ <Input bind:value={expiresIn} placeholder="7d, 1h, 30m" />
124
+ <span class="text-[10px] text-muted-foreground/60">Leave empty for no expiry</span>
125
+ </label>
126
+
127
+ {#if error}
128
+ <p class="text-xs text-destructive">{error}</p>
129
+ {/if}
130
+
131
+ <div class="flex gap-2 mt-1">
132
+ <Button type="submit" size="sm" class="flex-1" disabled={minting || !scopes.trim()}>
133
+ {minting ? 'Minting...' : 'Mint'}
134
+ </Button>
135
+ <Button variant="outline" size="sm" class="flex-1" onclick={onClose}>Cancel</Button>
136
+ </div>
137
+ </form>
138
+ {/if}
139
+ </div>
140
+ </div>
141
+ {/if}
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import { resolveRef } from '../zooid-uri';
3
+
4
+ let {
5
+ ref,
6
+ serverUrl = '',
7
+ onOpenRef,
8
+ }: {
9
+ ref: string;
10
+ serverUrl?: string;
11
+ onOpenRef?: (detail: { channel: string; eventId: string }) => void;
12
+ } = $props();
13
+
14
+ let resolved = $derived(resolveRef(ref, serverUrl));
15
+
16
+ function handleClick() {
17
+ if (resolved.type === 'zooid') {
18
+ onOpenRef?.({ channel: resolved.channel, eventId: resolved.eventId });
19
+ }
20
+ }
21
+ </script>
22
+
23
+ {#if resolved.type === 'zooid'}
24
+ <button class="text-xs font-mono text-primary/70 hover:text-primary bg-primary/10 hover:bg-primary/20 px-1.5 py-0 rounded cursor-pointer transition-colors" onclick={handleClick}>
25
+ {resolved.label}
26
+ </button>
27
+ {:else if resolved.type === 'zooid-external' || resolved.type === 'external'}
28
+ <a href={resolved.href} target="_blank" rel="noopener" class="text-xs font-mono text-primary/70 hover:text-primary bg-primary/10 hover:bg-primary/20 px-1.5 py-0 rounded transition-colors">
29
+ {resolved.label}
30
+ </a>
31
+ {:else}
32
+ <span class="text-xs font-mono text-muted-foreground">{resolved.label}</span>
33
+ {/if}
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ import type { ZooidEvent } from '../api';
3
+ import EventCard from './event-card.svelte';
4
+
5
+ let {
6
+ channel,
7
+ eventId,
8
+ client,
9
+ onClose,
10
+ }: {
11
+ channel: string;
12
+ eventId: string;
13
+ client: { poll: (channelId: string, options?: { cursor?: string; limit?: number }) => Promise<{ events: ZooidEvent[] }> };
14
+ onClose?: () => void;
15
+ } = $props();
16
+
17
+ let event = $state<ZooidEvent | null>(null);
18
+ let thread = $state<ZooidEvent[] | null>(null);
19
+ let loading = $state(true);
20
+ let threadExpanded = $state(false);
21
+
22
+ async function fetchEvent() {
23
+ loading = true;
24
+ try {
25
+ const res = await fetch(`/api/v1/channels/${channel}/events/${eventId}`);
26
+ if (res.ok) {
27
+ event = await res.json();
28
+ }
29
+ } catch {
30
+ // fetch failed
31
+ }
32
+ loading = false;
33
+ }
34
+
35
+ async function expandThread() {
36
+ try {
37
+ const res = await fetch(`/api/v1/channels/${channel}/events/${eventId}/thread`);
38
+ if (res.ok) {
39
+ const body = await res.json();
40
+ thread = body.events ?? [];
41
+ }
42
+ } catch {
43
+ // fetch failed
44
+ }
45
+ threadExpanded = true;
46
+ }
47
+
48
+ function handleKeydown(e: KeyboardEvent) {
49
+ if (e.key === 'Escape') onClose?.();
50
+ }
51
+
52
+ $effect(() => {
53
+ if (channel && eventId) fetchEvent();
54
+ });
55
+ </script>
56
+
57
+ <svelte:window onkeydown={handleKeydown} />
58
+
59
+ <button
60
+ type="button"
61
+ class="fixed inset-0 bg-background/40 z-40"
62
+ onclick={() => onClose?.()}
63
+ aria-label="Close ref panel"
64
+ ></button>
65
+
66
+ <div class="fixed inset-y-0 right-0 w-80 max-w-[85vw] bg-card border-l border-border shadow-lg z-50 p-4 flex flex-col gap-3 animate-slide-in-right overflow-y-auto">
67
+ <div class="flex items-center justify-between">
68
+ <h3 class="text-sm font-semibold">#{channel} / {eventId.slice(0, 8)}&hellip;</h3>
69
+ <button class="text-muted-foreground hover:text-foreground transition-colors" onclick={() => onClose?.()} aria-label="Close">
70
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
71
+ </button>
72
+ </div>
73
+
74
+ {#if loading}
75
+ <div class="text-sm text-muted-foreground">Loading&hellip;</div>
76
+ {:else if event}
77
+ <EventCard {event} />
78
+
79
+ {#if !threadExpanded}
80
+ <button class="text-xs text-primary hover:text-primary/80 transition-colors" onclick={expandThread}>
81
+ Expand thread
82
+ </button>
83
+ {:else if thread && thread.length > 0}
84
+ <div class="flex flex-col gap-1 border-t border-border pt-2">
85
+ {#each thread as threadEvent}
86
+ <EventCard event={threadEvent} />
87
+ {/each}
88
+ </div>
89
+ {:else if thread}
90
+ <div class="text-xs text-muted-foreground">No replies</div>
91
+ {/if}
92
+ {:else}
93
+ <div class="text-sm text-muted-foreground">Event not found</div>
94
+ {/if}
95
+ </div>
96
+
97
+ <style>
98
+ @keyframes slide-in-right {
99
+ from { transform: translateX(100%); }
100
+ to { transform: translateX(0); }
101
+ }
102
+ .animate-slide-in-right {
103
+ animation: slide-in-right 0.15s ease-out;
104
+ }
105
+ </style>
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import { Button } from '@ui/components/button/index';
3
+ import { Input } from '@ui/components/input/index';
4
+ import type { ZooidClient } from '@zooid/sdk';
5
+ import type { ServerIdentity } from '../api';
6
+
7
+ let {
8
+ open,
9
+ client,
10
+ onClose,
11
+ onSaved,
12
+ }: {
13
+ open: boolean;
14
+ client: ZooidClient;
15
+ onClose: () => void;
16
+ onSaved: () => void;
17
+ } = $props();
18
+
19
+ let loading = $state(false);
20
+ let saving = $state(false);
21
+ let error = $state('');
22
+
23
+ let name = $state('');
24
+ let description = $state('');
25
+ let tags = $state('');
26
+ let owner = $state('');
27
+ let company = $state('');
28
+ let email = $state('');
29
+
30
+ $effect(() => {
31
+ if (open) {
32
+ error = '';
33
+ loadMeta();
34
+ }
35
+ });
36
+
37
+ async function loadMeta() {
38
+ loading = true;
39
+ try {
40
+ const meta = await client.getServerMeta();
41
+ name = meta.name ?? '';
42
+ description = meta.description ?? '';
43
+ tags = (meta.tags ?? []).join(', ');
44
+ owner = meta.owner ?? '';
45
+ company = meta.company ?? '';
46
+ email = meta.email ?? '';
47
+ } catch (err) {
48
+ error = err instanceof Error ? err.message : 'Failed to load';
49
+ } finally {
50
+ loading = false;
51
+ }
52
+ }
53
+
54
+ async function handleSave(e: Event) {
55
+ e.preventDefault();
56
+ saving = true;
57
+ error = '';
58
+ try {
59
+ await client.updateServerMeta({
60
+ name: name || undefined,
61
+ description: description || null,
62
+ tags: tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : [],
63
+ owner: owner || null,
64
+ company: company || null,
65
+ email: email || null,
66
+ });
67
+ onSaved();
68
+ onClose();
69
+ } catch (err) {
70
+ error = err instanceof Error ? err.message : 'Failed to save';
71
+ } finally {
72
+ saving = false;
73
+ }
74
+ }
75
+
76
+ function handleBackdrop(e: MouseEvent) {
77
+ if (e.target === e.currentTarget) onClose();
78
+ }
79
+
80
+ function handleKeydown(e: KeyboardEvent) {
81
+ if (e.key === 'Escape') onClose();
82
+ }
83
+ </script>
84
+
85
+ {#if open}
86
+ <div
87
+ class="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
88
+ role="dialog"
89
+ aria-modal="true"
90
+ aria-label="Server configuration"
91
+ tabindex="-1"
92
+ onclick={handleBackdrop}
93
+ onkeydown={handleKeydown}
94
+ >
95
+ <div class="bg-card border border-border rounded-lg shadow-lg w-full max-w-sm p-6">
96
+ <h2 class="font-semibold text-sm mb-4">Server Configuration</h2>
97
+
98
+ {#if loading}
99
+ <p class="text-xs text-muted-foreground">Loading...</p>
100
+ {:else}
101
+ <form onsubmit={handleSave} class="flex flex-col gap-3">
102
+ <label class="flex flex-col gap-1">
103
+ <span class="text-xs text-muted-foreground">Name</span>
104
+ <Input bind:value={name} placeholder="My Zooid Server" />
105
+ </label>
106
+ <label class="flex flex-col gap-1">
107
+ <span class="text-xs text-muted-foreground">Description</span>
108
+ <Input bind:value={description} placeholder="What this server is for" />
109
+ </label>
110
+ <label class="flex flex-col gap-1">
111
+ <span class="text-xs text-muted-foreground">Tags (comma-separated)</span>
112
+ <Input bind:value={tags} placeholder="ai, signals, trading" />
113
+ </label>
114
+ <label class="flex flex-col gap-1">
115
+ <span class="text-xs text-muted-foreground">Owner</span>
116
+ <Input bind:value={owner} placeholder="Your name" />
117
+ </label>
118
+ <label class="flex flex-col gap-1">
119
+ <span class="text-xs text-muted-foreground">Company</span>
120
+ <Input bind:value={company} placeholder="Organization" />
121
+ </label>
122
+ <label class="flex flex-col gap-1">
123
+ <span class="text-xs text-muted-foreground">Email</span>
124
+ <Input bind:value={email} type="email" placeholder="contact@example.com" />
125
+ </label>
126
+
127
+ {#if error}
128
+ <p class="text-xs text-destructive">{error}</p>
129
+ {/if}
130
+
131
+ <div class="flex gap-2 mt-1">
132
+ <Button type="submit" size="sm" class="flex-1" disabled={saving}>
133
+ {saving ? 'Saving...' : 'Save'}
134
+ </Button>
135
+ <Button variant="outline" size="sm" class="flex-1" onclick={onClose}>Cancel</Button>
136
+ </div>
137
+ </form>
138
+ {/if}
139
+ </div>
140
+ </div>
141
+ {/if}
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ import { Button } from '@ui/components/button/index';
3
+ import { Input } from '@ui/components/input/index';
4
+ import type { ZooidClient } from '@zooid/sdk';
5
+
6
+ let {
7
+ client,
8
+ onMenuClick,
9
+ onSaved,
10
+ }: {
11
+ client: ZooidClient;
12
+ onMenuClick: () => void;
13
+ onSaved: () => void;
14
+ } = $props();
15
+
16
+ let loading = $state(false);
17
+ let saving = $state(false);
18
+ let error = $state('');
19
+ let saved = $state(false);
20
+
21
+ let name = $state('');
22
+ let description = $state('');
23
+ let tags = $state('');
24
+ let owner = $state('');
25
+ let company = $state('');
26
+ let email = $state('');
27
+
28
+ async function loadMeta() {
29
+ loading = true;
30
+ try {
31
+ const meta = await client.getServerMeta();
32
+ name = meta.name ?? '';
33
+ description = meta.description ?? '';
34
+ tags = (meta.tags ?? []).join(', ');
35
+ owner = meta.owner ?? '';
36
+ company = meta.company ?? '';
37
+ email = meta.email ?? '';
38
+ } catch (err) {
39
+ error = err instanceof Error ? err.message : 'Failed to load';
40
+ } finally {
41
+ loading = false;
42
+ }
43
+ }
44
+
45
+ async function handleSave(e: Event) {
46
+ e.preventDefault();
47
+ saving = true;
48
+ error = '';
49
+ saved = false;
50
+ try {
51
+ await client.updateServerMeta({
52
+ name: name || undefined,
53
+ description: description || null,
54
+ tags: tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : [],
55
+ owner: owner || null,
56
+ company: company || null,
57
+ email: email || null,
58
+ });
59
+ saved = true;
60
+ onSaved();
61
+ setTimeout(() => { saved = false; }, 2000);
62
+ } catch (err) {
63
+ error = err instanceof Error ? err.message : 'Failed to save';
64
+ } finally {
65
+ saving = false;
66
+ }
67
+ }
68
+
69
+ loadMeta();
70
+ </script>
71
+
72
+ <div class="flex-1 flex flex-col min-w-0">
73
+ <!-- Header -->
74
+ <div class="flex items-center gap-3 px-4 h-12 border-b border-border shrink-0">
75
+ <button
76
+ onclick={onMenuClick}
77
+ class="p-1 rounded hover:bg-secondary transition-colors md:hidden"
78
+ aria-label="Open channels"
79
+ >
80
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
81
+ </button>
82
+ <h1 class="text-sm font-semibold">Server Configuration</h1>
83
+ </div>
84
+
85
+ <!-- Content -->
86
+ <div class="flex-1 overflow-y-auto p-4 md:p-6">
87
+ <div class="max-w-lg mx-auto">
88
+ {#if loading}
89
+ <p class="text-xs text-muted-foreground">Loading...</p>
90
+ {:else}
91
+ <form onsubmit={handleSave} class="flex flex-col gap-3">
92
+ <label class="flex flex-col gap-1">
93
+ <span class="text-xs text-muted-foreground">Name</span>
94
+ <Input bind:value={name} placeholder="My Zooid Server" />
95
+ </label>
96
+ <label class="flex flex-col gap-1">
97
+ <span class="text-xs text-muted-foreground">Description</span>
98
+ <Input bind:value={description} placeholder="What this server is for" />
99
+ </label>
100
+ <label class="flex flex-col gap-1">
101
+ <span class="text-xs text-muted-foreground">Tags (comma-separated)</span>
102
+ <Input bind:value={tags} placeholder="ai, signals, trading" />
103
+ </label>
104
+ <label class="flex flex-col gap-1">
105
+ <span class="text-xs text-muted-foreground">Owner</span>
106
+ <Input bind:value={owner} placeholder="Your name" />
107
+ </label>
108
+ <label class="flex flex-col gap-1">
109
+ <span class="text-xs text-muted-foreground">Company</span>
110
+ <Input bind:value={company} placeholder="Organization" />
111
+ </label>
112
+ <label class="flex flex-col gap-1">
113
+ <span class="text-xs text-muted-foreground">Email</span>
114
+ <Input bind:value={email} type="email" placeholder="contact@example.com" />
115
+ </label>
116
+
117
+ {#if error}
118
+ <p class="text-xs text-destructive">{error}</p>
119
+ {/if}
120
+
121
+ <div class="flex gap-2 mt-1">
122
+ <Button type="submit" size="sm" disabled={saving}>
123
+ {saved ? 'Saved!' : saving ? 'Saving...' : 'Save'}
124
+ </Button>
125
+ </div>
126
+ </form>
127
+ {/if}
128
+ </div>
129
+ </div>
130
+ </div>