@zooid/web 0.6.0 → 0.7.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 (47) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +39 -0
  3. package/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  4. package/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  5. package/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  6. package/dist/assets/index-7XS-VDDg.js +118066 -0
  7. package/dist/assets/index-BZqMJwGi.css +1 -0
  8. package/dist/assets/index-BnCMAXt6.js +439 -0
  9. package/dist/assets/reaction-picker-emoji-GenoruKr.js +716 -0
  10. package/dist/favicon.svg +1 -0
  11. package/dist/index.html +7 -35
  12. package/package.json +62 -29
  13. package/dist/assets/index-7P1i28a8.js +0 -66
  14. package/dist/assets/index-BeqmFlCW.css +0 -1
  15. package/dist/assets/json-editor-Cvlnnf1Q.css +0 -1
  16. package/dist/assets/json-editor-iJIPDKxB.js +0 -84
  17. package/dist/assets/vanilla-picker-l5rcX3cq.js +0 -8
  18. package/src/App.svelte +0 -678
  19. package/src/app.css +0 -178
  20. package/src/lib/api.ts +0 -78
  21. package/src/lib/components/admin-dropdown.svelte +0 -89
  22. package/src/lib/components/auth-modal.svelte +0 -114
  23. package/src/lib/components/avatar.svelte +0 -29
  24. package/src/lib/components/channel-header.svelte +0 -73
  25. package/src/lib/components/create-channel-modal.svelte +0 -137
  26. package/src/lib/components/edit-channel-modal.svelte +0 -234
  27. package/src/lib/components/event-card.svelte +0 -221
  28. package/src/lib/components/event-feed.svelte +0 -50
  29. package/src/lib/components/homepage.svelte +0 -86
  30. package/src/lib/components/json-editor.svelte +0 -57
  31. package/src/lib/components/keys-and-tokens-page.svelte +0 -216
  32. package/src/lib/components/keys-modal.svelte +0 -120
  33. package/src/lib/components/message-bar.svelte +0 -290
  34. package/src/lib/components/mint-token-modal.svelte +0 -141
  35. package/src/lib/components/ref-link.svelte +0 -33
  36. package/src/lib/components/ref-side-sheet.svelte +0 -105
  37. package/src/lib/components/server-config-modal.svelte +0 -141
  38. package/src/lib/components/server-config-page.svelte +0 -130
  39. package/src/lib/components/sidebar.svelte +0 -144
  40. package/src/lib/components/status-bar.svelte +0 -40
  41. package/src/lib/pretty-json.test.ts +0 -200
  42. package/src/lib/pretty-json.ts +0 -79
  43. package/src/lib/time.ts +0 -38
  44. package/src/lib/zooid-uri.test.ts +0 -102
  45. package/src/lib/zooid-uri.ts +0 -74
  46. package/src/main.ts +0 -7
  47. package/src/vite-env.d.ts +0 -2
@@ -1,86 +0,0 @@
1
- <script lang="ts">
2
- import { Badge } from '@ui/components/badge/index';
3
- import { Card, CardContent } from '@ui/components/card/index';
4
- import { Separator } from '@ui/components/separator/index';
5
- import { fetchServerMeta, listChannels, type ChannelInfo } from '../api';
6
- import { formatRelativeUlid } from '../time';
7
-
8
- const baseUrl = window.location.origin;
9
-
10
- let serverName = $state('Zooid');
11
- let serverDesc = $state<string | null>(null);
12
- let channels = $state<ChannelInfo[]>([]);
13
- let loading = $state(true);
14
-
15
- async function load() {
16
- const [meta, chs] = await Promise.all([
17
- fetchServerMeta(baseUrl),
18
- listChannels(baseUrl),
19
- ]);
20
- serverName = meta.server_name;
21
- serverDesc = meta.server_description;
22
- channels = chs;
23
- loading = false;
24
- }
25
-
26
- load();
27
-
28
- </script>
29
-
30
- <div class="min-h-screen max-w-2xl mx-auto px-4 py-12 flex flex-col">
31
- <header class="mb-10">
32
- <h1 class="text-2xl font-bold tracking-tight mb-1">{serverName}</h1>
33
- <p class="text-sm text-muted-foreground">{serverDesc ?? 'Channels on this server:'}</p>
34
- </header>
35
-
36
- {#if loading}
37
- <p class="text-sm text-muted-foreground">Loading channels...</p>
38
- {:else if channels.length === 0}
39
- <div class="text-sm text-muted-foreground border border-dashed border-border rounded-lg p-8 text-center">
40
- <p>No channels yet.</p>
41
- <p class="mt-1">Create one with <code class="text-foreground">npx zooid channel create</code></p>
42
- </div>
43
- {:else}
44
- <div class="flex flex-col gap-3">
45
- {#each channels as ch (ch.id)}
46
- <a href="/{ch.id}" class="block group no-underline">
47
- <Card class="transition-colors group-hover:border-primary/40">
48
- <CardContent class="p-4">
49
- <div class="flex items-center justify-between gap-2 mb-1">
50
- <div class="flex items-center gap-2">
51
- <span class="font-semibold text-sm">{ch.name}</span>
52
- {#if ch.is_public}
53
- <Badge variant="secondary" class="text-[10px] px-1.5 py-0">public</Badge>
54
- {:else}
55
- <Badge variant="outline" class="text-[10px] px-1.5 py-0">private</Badge>
56
- {/if}
57
- </div>
58
- <span class="text-[10px] text-muted-foreground shrink-0">
59
- {ch.event_count} event{ch.event_count === 1 ? '' : 's'}
60
- </span>
61
- </div>
62
-
63
- {#if ch.description}
64
- <p class="text-xs text-muted-foreground mb-2">{ch.description}</p>
65
- {/if}
66
-
67
- <div class="flex items-center gap-3 text-[10px] text-muted-foreground/60">
68
- <span class="font-mono">{ch.id}</span>
69
- {#if ch.last_event_id}
70
- <Separator orientation="vertical" class="h-3" />
71
- <span>latest {formatRelativeUlid(ch.last_event_id)}</span>
72
- {/if}
73
- </div>
74
- </CardContent>
75
- </Card>
76
- </a>
77
- {/each}
78
- </div>
79
- {/if}
80
-
81
- <div class="flex-1"></div>
82
- <footer class="mt-12 pt-4 pb-[env(safe-area-inset-bottom)] border-t border-border text-[10px] text-muted-foreground/40 flex items-center justify-between">
83
- <span>Powered by <a href="https://zooid.dev" class="underline hover:text-muted-foreground">Zooid</a></span>
84
- <a href="https://github.com/zooid-ai/zooid" class="underline hover:text-muted-foreground">Star on GitHub</a>
85
- </footer>
86
- </div>
@@ -1,57 +0,0 @@
1
- <script lang="ts">
2
- import { JSONEditor, createAjvValidator, Mode } from 'svelte-jsoneditor';
3
- import type { Content } from 'svelte-jsoneditor';
4
-
5
- let {
6
- content = $bindable({ json: {} }),
7
- schema,
8
- }: {
9
- content: Content;
10
- schema?: Record<string, unknown> | null;
11
- } = $props();
12
-
13
- let validator = $derived.by(() => {
14
- if (!schema) return undefined;
15
- try {
16
- return createAjvValidator({ schema });
17
- } catch {
18
- return undefined;
19
- }
20
- });
21
- </script>
22
-
23
- <div class="json-editor-wrapper">
24
- <JSONEditor
25
- bind:content
26
- mode={Mode.text}
27
- mainMenuBar={false}
28
- navigationBar={false}
29
- statusBar={false}
30
- {validator}
31
- />
32
- </div>
33
-
34
- <style>
35
- .json-editor-wrapper {
36
- --jse-theme-color: oklch(0.21 0 0);
37
- --jse-theme-color-highlight: oklch(0.25 0 0);
38
- --jse-background-color: oklch(0.18 0 0);
39
- --jse-text-color: oklch(0.85 0 0);
40
- --jse-panel-background: oklch(0.15 0 0);
41
- --jse-panel-border: oklch(0.25 0 0);
42
- --jse-main-border: 1px solid oklch(0.25 0 0);
43
- --jse-key-color: oklch(0.7 0.1 200);
44
- --jse-value-color-string: oklch(0.75 0.1 150);
45
- --jse-value-color-number: oklch(0.75 0.1 60);
46
- --jse-delimiter-color: oklch(0.5 0 0);
47
- --jse-error-color: oklch(0.65 0.2 25);
48
- border-radius: 0.375rem;
49
- overflow: hidden;
50
- max-height: 150px;
51
- }
52
-
53
- .json-editor-wrapper :global(.jse-text-mode) {
54
- min-height: 60px;
55
- max-height: 150px;
56
- }
57
- </style>
@@ -1,216 +0,0 @@
1
- <script lang="ts">
2
- import { Button } from '@ui/components/button/index';
3
- import { Input } from '@ui/components/input/index';
4
- import type { ZooidClient, TrustedKey } from '@zooid/sdk';
5
- import { formatRelative } from '../time';
6
-
7
- let {
8
- client,
9
- onMenuClick,
10
- }: {
11
- client: ZooidClient;
12
- onMenuClick: () => void;
13
- } = $props();
14
-
15
- // --- Keys ---
16
- let keys = $state<TrustedKey[]>([]);
17
- let keysLoading = $state(false);
18
- let keysError = $state('');
19
- let revoking = $state<string | null>(null);
20
-
21
- async function loadKeys() {
22
- keysLoading = true;
23
- keysError = '';
24
- try {
25
- keys = await client.listKeys();
26
- } catch (err) {
27
- keysError = err instanceof Error ? err.message : 'Failed to load keys';
28
- } finally {
29
- keysLoading = false;
30
- }
31
- }
32
-
33
- async function handleRevoke(kid: string) {
34
- if (!confirm(`Revoke key "${kid}"? Tokens signed by this key will stop working.`)) return;
35
- revoking = kid;
36
- keysError = '';
37
- try {
38
- await client.revokeKey(kid);
39
- keys = keys.filter((k) => k.kid !== kid);
40
- } catch (err) {
41
- keysError = err instanceof Error ? err.message : 'Failed to revoke key';
42
- } finally {
43
- revoking = null;
44
- }
45
- }
46
-
47
- // --- Mint token ---
48
- let scopes = $state('pub:*, sub:*');
49
- let sub = $state('');
50
- let name = $state('');
51
- let expiresIn = $state('');
52
- let minting = $state(false);
53
- let mintError = $state('');
54
- let mintedToken = $state('');
55
- let copied = $state(false);
56
-
57
- async function handleMint(e: Event) {
58
- e.preventDefault();
59
- minting = true;
60
- mintError = '';
61
- mintedToken = '';
62
- copied = false;
63
-
64
- const scopeList = scopes.split(',').map((s) => s.trim()).filter(Boolean);
65
- if (scopeList.length === 0) {
66
- mintError = 'At least one scope is required';
67
- minting = false;
68
- return;
69
- }
70
-
71
- try {
72
- const result = await client.mintToken({
73
- scopes: scopeList,
74
- sub: sub || undefined,
75
- name: name || undefined,
76
- expires_in: expiresIn || undefined,
77
- });
78
- mintedToken = result.token;
79
- } catch (err) {
80
- mintError = err instanceof Error ? err.message : 'Failed to mint token';
81
- } finally {
82
- minting = false;
83
- }
84
- }
85
-
86
- async function copyToken() {
87
- await navigator.clipboard.writeText(mintedToken);
88
- copied = true;
89
- setTimeout(() => { copied = false; }, 2000);
90
- }
91
-
92
- function resetMint() {
93
- mintedToken = '';
94
- mintError = '';
95
- copied = false;
96
- }
97
-
98
- // Load keys on mount
99
- loadKeys();
100
- </script>
101
-
102
- <div class="flex-1 flex flex-col min-w-0">
103
- <!-- Header -->
104
- <div class="flex items-center gap-3 px-4 h-12 border-b border-border shrink-0">
105
- <button
106
- onclick={onMenuClick}
107
- class="p-1 rounded hover:bg-secondary transition-colors md:hidden"
108
- aria-label="Open channels"
109
- >
110
- <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>
111
- </button>
112
- <h1 class="text-sm font-semibold">Keys & Tokens</h1>
113
- </div>
114
-
115
- <!-- Content -->
116
- <div class="flex-1 overflow-y-auto p-4 md:p-6">
117
- <div class="max-w-lg mx-auto flex flex-col gap-8">
118
-
119
- <!-- Mint Token section -->
120
- <section>
121
- <h2 class="font-semibold text-sm mb-1">Mint Token</h2>
122
- <p class="text-xs text-muted-foreground mb-4">Create a new JWT with custom scopes.</p>
123
-
124
- {#if mintedToken}
125
- <div class="flex flex-col gap-3">
126
- <div class="bg-secondary rounded-md p-3">
127
- <div class="text-[10px] text-muted-foreground mb-1 uppercase tracking-wider">Token (shown once)</div>
128
- <div class="text-xs font-mono break-all select-all max-h-24 overflow-y-auto">{mintedToken}</div>
129
- </div>
130
- <div class="flex gap-2">
131
- <Button size="sm" class="flex-1" onclick={copyToken}>
132
- {copied ? 'Copied!' : 'Copy'}
133
- </Button>
134
- <Button variant="outline" size="sm" class="flex-1" onclick={resetMint}>Mint another</Button>
135
- </div>
136
- </div>
137
- {:else}
138
- <form onsubmit={handleMint} class="flex flex-col gap-3">
139
- <label class="flex flex-col gap-1">
140
- <span class="text-xs text-muted-foreground">Scopes (comma-separated)</span>
141
- <Input bind:value={scopes} placeholder="admin, pub:my-channel, sub:*" />
142
- <span class="text-[10px] text-muted-foreground/60">admin, pub:channel, sub:channel, pub:*, sub:*</span>
143
- </label>
144
- <label class="flex flex-col gap-1">
145
- <span class="text-xs text-muted-foreground">Subject (optional)</span>
146
- <Input bind:value={sub} placeholder="my-bot" />
147
- </label>
148
- <label class="flex flex-col gap-1">
149
- <span class="text-xs text-muted-foreground">Display name (optional)</span>
150
- <Input bind:value={name} placeholder="My Bot" />
151
- </label>
152
- <label class="flex flex-col gap-1">
153
- <span class="text-xs text-muted-foreground">Expires in (optional)</span>
154
- <Input bind:value={expiresIn} placeholder="7d, 1h, 30m" />
155
- <span class="text-[10px] text-muted-foreground/60">Leave empty for no expiry</span>
156
- </label>
157
-
158
- {#if mintError}
159
- <p class="text-xs text-destructive">{mintError}</p>
160
- {/if}
161
-
162
- <div class="flex gap-2 mt-1">
163
- <Button type="submit" size="sm" disabled={minting || !scopes.trim()}>
164
- {minting ? 'Minting...' : 'Mint'}
165
- </Button>
166
- </div>
167
- </form>
168
- {/if}
169
- </section>
170
-
171
- <!-- Signing Keys section -->
172
- <section>
173
- <h2 class="font-semibold text-sm mb-1">Signing Keys</h2>
174
- <p class="text-xs text-muted-foreground mb-4">Trusted keys used to verify JWT tokens.</p>
175
-
176
- {#if keysLoading}
177
- <p class="text-xs text-muted-foreground">Loading...</p>
178
- {:else if keys.length === 0}
179
- <p class="text-xs text-muted-foreground/60 text-center py-4">No keys found</p>
180
- {:else}
181
- <div class="flex flex-col gap-2">
182
- {#each keys as key (key.kid)}
183
- <div class="bg-secondary rounded-md px-3 py-2 flex items-start justify-between gap-2">
184
- <div class="min-w-0 flex-1">
185
- <div class="text-xs font-mono font-medium truncate">{key.kid}</div>
186
- <div class="text-[10px] text-muted-foreground mt-0.5">
187
- {key.crv} · {key.issuer ?? 'unknown issuer'} · {formatRelative(key.created_at)}
188
- </div>
189
- {#if key.max_scopes}
190
- <div class="text-[10px] text-muted-foreground/60 mt-0.5 font-mono truncate">
191
- {key.max_scopes.join(', ')}
192
- </div>
193
- {/if}
194
- </div>
195
- <Button
196
- variant="destructive"
197
- size="sm"
198
- class="shrink-0 text-[10px] h-6 px-2"
199
- disabled={revoking === key.kid}
200
- onclick={() => handleRevoke(key.kid)}
201
- >
202
- {revoking === key.kid ? '...' : 'Revoke'}
203
- </Button>
204
- </div>
205
- {/each}
206
- </div>
207
- {/if}
208
-
209
- {#if keysError}
210
- <p class="text-xs text-destructive mt-3">{keysError}</p>
211
- {/if}
212
- </section>
213
-
214
- </div>
215
- </div>
216
- </div>
@@ -1,120 +0,0 @@
1
- <script lang="ts">
2
- import { Button } from '@ui/components/button/index';
3
- import type { ZooidClient, TrustedKey } from '@zooid/sdk';
4
- import { formatRelative } from '../time';
5
-
6
- let {
7
- open,
8
- client,
9
- onClose,
10
- }: {
11
- open: boolean;
12
- client: ZooidClient;
13
- onClose: () => void;
14
- } = $props();
15
-
16
- let keys = $state<TrustedKey[]>([]);
17
- let loading = $state(false);
18
- let error = $state('');
19
- let revoking = $state<string | null>(null);
20
-
21
- $effect(() => {
22
- if (open) {
23
- error = '';
24
- loadKeys();
25
- }
26
- });
27
-
28
- async function loadKeys() {
29
- loading = true;
30
- try {
31
- keys = await client.listKeys();
32
- } catch (err) {
33
- error = err instanceof Error ? err.message : 'Failed to load keys';
34
- } finally {
35
- loading = false;
36
- }
37
- }
38
-
39
- async function handleRevoke(kid: string) {
40
- if (!confirm(`Revoke key "${kid}"? Tokens signed by this key will stop working.`)) return;
41
- revoking = kid;
42
- error = '';
43
- try {
44
- await client.revokeKey(kid);
45
- keys = keys.filter((k) => k.kid !== kid);
46
- } catch (err) {
47
- error = err instanceof Error ? err.message : 'Failed to revoke key';
48
- } finally {
49
- revoking = null;
50
- }
51
- }
52
-
53
- function handleBackdrop(e: MouseEvent) {
54
- if (e.target === e.currentTarget) onClose();
55
- }
56
-
57
- function handleKeydown(e: KeyboardEvent) {
58
- if (e.key === 'Escape') onClose();
59
- }
60
- </script>
61
-
62
- {#if open}
63
- <div
64
- class="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
65
- role="dialog"
66
- aria-modal="true"
67
- aria-label="Signing keys"
68
- tabindex="-1"
69
- onclick={handleBackdrop}
70
- onkeydown={handleKeydown}
71
- >
72
- <div class="bg-card border border-border rounded-lg shadow-lg w-full max-w-md p-6">
73
- <h2 class="font-semibold text-sm mb-1">Signing Keys</h2>
74
- <p class="text-xs text-muted-foreground mb-4">
75
- Trusted keys used to verify JWT tokens.
76
- </p>
77
-
78
- {#if loading}
79
- <p class="text-xs text-muted-foreground">Loading...</p>
80
- {:else if keys.length === 0}
81
- <p class="text-xs text-muted-foreground/60 text-center py-4">No keys found</p>
82
- {:else}
83
- <div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
84
- {#each keys as key (key.kid)}
85
- <div class="bg-secondary rounded-md px-3 py-2 flex items-start justify-between gap-2">
86
- <div class="min-w-0 flex-1">
87
- <div class="text-xs font-mono font-medium truncate">{key.kid}</div>
88
- <div class="text-[10px] text-muted-foreground mt-0.5">
89
- {key.crv} · {key.issuer ?? 'unknown issuer'} · {formatRelative(key.created_at)}
90
- </div>
91
- {#if key.max_scopes}
92
- <div class="text-[10px] text-muted-foreground/60 mt-0.5 font-mono truncate">
93
- {key.max_scopes.join(', ')}
94
- </div>
95
- {/if}
96
- </div>
97
- <Button
98
- variant="destructive"
99
- size="sm"
100
- class="shrink-0 text-[10px] h-6 px-2"
101
- disabled={revoking === key.kid}
102
- onclick={() => handleRevoke(key.kid)}
103
- >
104
- {revoking === key.kid ? '...' : 'Revoke'}
105
- </Button>
106
- </div>
107
- {/each}
108
- </div>
109
- {/if}
110
-
111
- {#if error}
112
- <p class="text-xs text-destructive mt-3">{error}</p>
113
- {/if}
114
-
115
- <div class="flex justify-end mt-4">
116
- <Button variant="outline" size="sm" onclick={onClose}>Close</Button>
117
- </div>
118
- </div>
119
- </div>
120
- {/if}