@zooid/web 0.6.0 → 0.8.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-Bq2HBBZQ.css +1 -0
  7. package/dist/assets/index-CiUWUtw6.js +118066 -0
  8. package/dist/assets/index-wOYm83VW.js +439 -0
  9. package/dist/assets/reaction-picker-emoji-CI7LVLOX.js +716 -0
  10. package/dist/favicon.svg +1 -0
  11. package/dist/index.html +7 -35
  12. package/package.json +63 -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
package/src/app.css DELETED
@@ -1,178 +0,0 @@
1
- @import 'tailwindcss';
2
- @import '@zooid/ui/styles/reef.css';
3
- @source '../../ui/src';
4
-
5
- @custom-variant dark (&:where(.dark, .dark *));
6
-
7
- @theme {
8
- --color-background: var(--reef-bg);
9
- --color-foreground: var(--reef-fg);
10
- --color-card: var(--reef-gray-600);
11
- --color-card-foreground: var(--reef-fg);
12
- --color-popover: var(--reef-gray-600);
13
- --color-popover-foreground: var(--reef-fg);
14
- --color-primary: var(--reef-blue);
15
- --color-primary-foreground: var(--reef-bg);
16
- --color-secondary: var(--reef-gray-500);
17
- --color-secondary-foreground: var(--reef-fg);
18
- --color-muted: var(--reef-gray-500);
19
- --color-muted-foreground: var(--reef-gray-300);
20
- --color-accent: var(--reef-gray-500);
21
- --color-accent-foreground: var(--reef-fg);
22
- --color-destructive: var(--reef-red);
23
- --color-destructive-foreground: var(--reef-fg);
24
- --color-border: var(--reef-gray-500);
25
- --color-input: var(--reef-gray-500);
26
- --color-ring: var(--reef-blue);
27
-
28
- --radius-sm: 0.25rem;
29
- --radius-md: 0.375rem;
30
- --radius-lg: 0.5rem;
31
- --radius-xl: 0.75rem;
32
- }
33
-
34
- body {
35
- background-color: var(--color-background);
36
- color: var(--color-foreground);
37
- font-family: var(--reef-font-mono);
38
- }
39
-
40
- .overflow-wrap-anywhere {
41
- overflow-wrap: anywhere;
42
- }
43
-
44
- .prose-inline {
45
- display: inline;
46
- }
47
- .prose-inline > :first-child {
48
- display: inline;
49
- }
50
- .prose-inline p {
51
- display: inline;
52
- }
53
- .prose-inline ul,
54
- .prose-inline ol {
55
- margin: 0.25rem 0;
56
- padding-left: 1.25rem;
57
- }
58
- .prose-inline li {
59
- margin: 0.125rem 0;
60
- }
61
- .prose-inline strong {
62
- font-weight: 700;
63
- color: var(--color-foreground);
64
- }
65
- .prose-inline em {
66
- font-style: italic;
67
- }
68
- .prose-inline a {
69
- color: var(--color-primary);
70
- text-decoration: underline;
71
- text-underline-offset: 2px;
72
- }
73
- .prose-inline a:hover {
74
- opacity: 0.8;
75
- }
76
- .prose-inline code {
77
- background: var(--color-secondary);
78
- border-radius: 0.25rem;
79
- padding: 0.1rem 0.3rem;
80
- font-size: 0.85em;
81
- }
82
- .prose-inline pre {
83
- background: var(--color-secondary);
84
- border-radius: 0.375rem;
85
- padding: 0.5rem;
86
- margin: 0.25rem 0;
87
- overflow-x: auto;
88
- }
89
- .prose-inline blockquote {
90
- border-left: 2px solid var(--color-border);
91
- padding-left: 0.75rem;
92
- margin: 0.25rem 0;
93
- opacity: 0.8;
94
- }
95
- .prose-inline h1,
96
- .prose-inline h2,
97
- .prose-inline h3,
98
- .prose-inline h4,
99
- .prose-inline h5,
100
- .prose-inline h6 {
101
- font-weight: 700;
102
- color: var(--color-foreground);
103
- margin: 0.25rem 0;
104
- }
105
-
106
- /* Block-level markdown for message bodies */
107
- .prose-block p {
108
- margin: 0.375rem 0;
109
- }
110
- .prose-block p:first-child {
111
- margin-top: 0;
112
- }
113
- .prose-block p:last-child {
114
- margin-bottom: 0;
115
- }
116
- .prose-block strong {
117
- font-weight: 700;
118
- color: var(--color-foreground);
119
- }
120
- .prose-block em {
121
- font-style: italic;
122
- }
123
- .prose-block a {
124
- color: var(--color-primary);
125
- text-decoration: underline;
126
- text-underline-offset: 2px;
127
- }
128
- .prose-block a:hover {
129
- opacity: 0.8;
130
- }
131
- .prose-block code {
132
- background: var(--color-secondary);
133
- border-radius: 0.25rem;
134
- padding: 0.1rem 0.3rem;
135
- font-size: 0.85em;
136
- }
137
- .prose-block pre {
138
- background: var(--color-secondary);
139
- border-radius: 0.375rem;
140
- padding: 0.5rem;
141
- margin: 0.375rem 0;
142
- overflow-x: auto;
143
- }
144
- .prose-block pre code {
145
- background: none;
146
- padding: 0;
147
- border-radius: 0;
148
- font-size: inherit;
149
- }
150
- .prose-block ul,
151
- .prose-block ol {
152
- margin: 0.375rem 0;
153
- padding-left: 1.25rem;
154
- }
155
- .prose-block li {
156
- margin: 0.125rem 0;
157
- }
158
- .prose-block blockquote {
159
- border-left: 2px solid var(--color-border);
160
- padding-left: 0.75rem;
161
- margin: 0.375rem 0;
162
- opacity: 0.8;
163
- }
164
- .prose-block h1,
165
- .prose-block h2,
166
- .prose-block h3,
167
- .prose-block h4,
168
- .prose-block h5,
169
- .prose-block h6 {
170
- font-weight: 700;
171
- color: var(--color-foreground);
172
- margin: 0.5rem 0 0.25rem;
173
- }
174
- .prose-block h1:first-child,
175
- .prose-block h2:first-child,
176
- .prose-block h3:first-child {
177
- margin-top: 0;
178
- }
package/src/lib/api.ts DELETED
@@ -1,78 +0,0 @@
1
- import { ZooidClient } from '@zooid/sdk';
2
-
3
- export type {
4
- ChannelInfo,
5
- ServerIdentity,
6
- TrustedKey,
7
- TokenClaims,
8
- ZooidEvent,
9
- PollResult,
10
- } from '@zooid/sdk';
11
-
12
- export interface ServerMeta {
13
- server_name: string;
14
- server_description: string | null;
15
- poll_interval: number;
16
- delivery: string[];
17
- auth_url?: string;
18
- }
19
-
20
- const defaultMeta: ServerMeta = {
21
- server_name: 'Zooid',
22
- server_description: null,
23
- poll_interval: 5,
24
- delivery: ['poll'],
25
- };
26
-
27
- export async function fetchServerMeta(baseUrl: string): Promise<ServerMeta> {
28
- try {
29
- const res = await fetch(`${baseUrl}/.well-known/zooid.json`);
30
- if (!res.ok) return defaultMeta;
31
- const data = await res.json();
32
- return {
33
- server_name: data.server_name ?? 'Zooid',
34
- server_description: data.server_description ?? null,
35
- poll_interval: data.poll_interval ?? 5,
36
- delivery: Array.isArray(data.delivery) ? data.delivery : ['poll'],
37
- auth_url: data.auth_url ?? undefined,
38
- };
39
- } catch {
40
- return defaultMeta;
41
- }
42
- }
43
-
44
- /** Create a ZooidClient bound to the current origin. */
45
- export function createClient(token?: string): ZooidClient {
46
- return new ZooidClient({
47
- server: window.location.origin,
48
- token,
49
- });
50
- }
51
-
52
- // BFF auth endpoints (not in SDK — they use cookies/credentials)
53
-
54
- export async function refreshAuth(
55
- baseUrl: string,
56
- ): Promise<{ token: string } | null> {
57
- try {
58
- const res = await fetch(`${baseUrl}/api/v1/auth/refresh`, {
59
- method: 'POST',
60
- credentials: 'include',
61
- });
62
- if (!res.ok) return null;
63
- return res.json();
64
- } catch {
65
- return null;
66
- }
67
- }
68
-
69
- export async function authLogout(baseUrl: string): Promise<void> {
70
- try {
71
- await fetch(`${baseUrl}/api/v1/auth/logout`, {
72
- method: 'POST',
73
- credentials: 'include',
74
- });
75
- } catch {
76
- // Best effort
77
- }
78
- }
@@ -1,89 +0,0 @@
1
- <script lang="ts">
2
- import type { Component } from 'svelte';
3
-
4
- interface SettingsPageExtension {
5
- slug: string;
6
- label: string;
7
- icon?: Component;
8
- }
9
-
10
- let {
11
- serverName,
12
- onServerConfig,
13
- onKeysAndTokens,
14
- extensionSettingsPages = [],
15
- onExtensionSettings,
16
- }: {
17
- serverName: string;
18
- onServerConfig: () => void;
19
- onKeysAndTokens: () => void;
20
- extensionSettingsPages?: SettingsPageExtension[];
21
- onExtensionSettings?: (slug: string) => void;
22
- } = $props();
23
-
24
- let open = $state(false);
25
-
26
- function toggle() {
27
- open = !open;
28
- }
29
-
30
- function select(fn: () => void) {
31
- open = false;
32
- fn();
33
- }
34
-
35
- function handleBackdrop() {
36
- open = false;
37
- }
38
- </script>
39
-
40
- <div class="relative">
41
- <button
42
- onclick={toggle}
43
- class="flex items-center gap-1.5 font-semibold text-sm truncate hover:text-primary transition-colors"
44
- title="Server settings"
45
- >
46
- <span class="truncate">{serverName}</span>
47
- <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 text-muted-foreground"><path d="m6 9 6 6 6-6"/></svg>
48
- </button>
49
-
50
- {#if open}
51
- <!-- Backdrop -->
52
- <button
53
- type="button"
54
- class="fixed inset-0 z-40"
55
- onclick={handleBackdrop}
56
- aria-label="Close menu"
57
- tabindex="-1"
58
- ></button>
59
-
60
- <!-- Dropdown -->
61
- <div class="absolute top-full left-0 mt-1 w-48 bg-card border border-border rounded-md shadow-lg z-50 py-1">
62
- <button
63
- class="w-full text-left px-3 py-1.5 text-xs hover:bg-secondary transition-colors flex items-center gap-2"
64
- onclick={() => select(onServerConfig)}
65
- >
66
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
67
- Server config
68
- </button>
69
- <button
70
- class="w-full text-left px-3 py-1.5 text-xs hover:bg-secondary transition-colors flex items-center gap-2"
71
- onclick={() => select(onKeysAndTokens)}
72
- >
73
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>
74
- Keys & tokens
75
- </button>
76
- {#each extensionSettingsPages as page}
77
- <button
78
- class="w-full text-left px-3 py-1.5 text-xs hover:bg-secondary transition-colors flex items-center gap-2"
79
- onclick={() => { if (onExtensionSettings) select(() => onExtensionSettings(page.slug)); }}
80
- >
81
- {#if page.icon}
82
- <svelte:component this={page.icon} size={14} />
83
- {/if}
84
- {page.label}
85
- </button>
86
- {/each}
87
- </div>
88
- {/if}
89
- </div>
@@ -1,114 +0,0 @@
1
- <script lang="ts">
2
- import { Button } from '@ui/components/button/index';
3
- import { Input } from '@ui/components/input/index';
4
-
5
- import type { TokenClaims } from '../api';
6
-
7
- let {
8
- open,
9
- currentToken,
10
- claims,
11
- onSave,
12
- onLogout,
13
- onClose,
14
- }: {
15
- open: boolean;
16
- currentToken: string | null;
17
- claims: TokenClaims | null;
18
- onSave: (token: string) => Promise<boolean>;
19
- onLogout: () => void;
20
- onClose: () => void;
21
- } = $props();
22
-
23
- let tokenInput = $state('');
24
- let error = $state('');
25
- let loading = $state(false);
26
-
27
- async function handleSubmit(e: Event) {
28
- e.preventDefault();
29
- const trimmed = tokenInput.trim();
30
- if (!trimmed || loading) return;
31
-
32
- error = '';
33
- loading = true;
34
- try {
35
- const ok = await onSave(trimmed);
36
- if (!ok) {
37
- error = 'Invalid token. Check that it is correct and not expired.';
38
- } else {
39
- tokenInput = '';
40
- }
41
- } catch {
42
- error = 'Failed to validate token. Is the server reachable?';
43
- } finally {
44
- loading = false;
45
- }
46
- }
47
-
48
- function handleBackdrop(e: MouseEvent) {
49
- if (e.target === e.currentTarget) onClose();
50
- }
51
-
52
- function handleKeydown(e: KeyboardEvent) {
53
- if (e.key === 'Escape') onClose();
54
- }
55
-
56
- // Clear error when modal reopens or input changes
57
- $effect(() => {
58
- if (open) error = '';
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="Authentication"
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-sm p-6">
73
- <h2 class="font-semibold text-sm mb-1">Authentication</h2>
74
- <p class="text-xs text-muted-foreground mb-4">
75
- Paste an admin or publish token to unlock channels and publishing.
76
- </p>
77
-
78
- {#if currentToken && claims}
79
- <div class="bg-secondary rounded-md px-3 py-2 mb-4">
80
- <div class="text-sm font-medium text-foreground">
81
- Signed in as {claims.name || claims.sub || 'Unknown'}
82
- </div>
83
- <div class="text-xs text-muted-foreground mt-1 font-mono">
84
- {claims.scopes.join(', ')}
85
- </div>
86
- </div>
87
- <div class="flex gap-2">
88
- <Button variant="destructive" size="sm" class="flex-1" onclick={onLogout}>Sign out</Button>
89
- <Button variant="outline" size="sm" class="flex-1" onclick={onClose}>Close</Button>
90
- </div>
91
- {:else}
92
- <form onsubmit={handleSubmit} class="flex flex-col gap-3">
93
- <Input
94
- type="text"
95
- placeholder="eyJhbGci..."
96
- bind:value={tokenInput}
97
- oninput={() => error = ''}
98
- autocomplete="off"
99
- spellcheck="false"
100
- />
101
- {#if error}
102
- <p class="text-xs text-destructive">{error}</p>
103
- {/if}
104
- <div class="flex gap-2">
105
- <Button type="submit" size="sm" class="flex-1" disabled={!tokenInput.trim() || loading}>
106
- {loading ? 'Verifying...' : 'Sign in'}
107
- </Button>
108
- <Button variant="outline" size="sm" class="flex-1" onclick={onClose}>Cancel</Button>
109
- </div>
110
- </form>
111
- {/if}
112
- </div>
113
- </div>
114
- {/if}
@@ -1,29 +0,0 @@
1
- <script lang="ts">
2
- import { createAvatar } from '@dicebear/core';
3
- import { glass } from '@dicebear/collection';
4
- import type { ZooidEvent } from '../api';
5
-
6
- let { event, size = 24 }: { event: ZooidEvent; size?: number } = $props();
7
-
8
- let seed = $derived(avatarSeed(event));
9
- let svg = $derived(createAvatar(glass, { seed, size }).toString());
10
-
11
- function avatarSeed(e: ZooidEvent): string {
12
- const id = e.publisher_id;
13
- const name = e.publisher_name;
14
-
15
- if (!id) return name ?? 'anonymous';
16
-
17
- const colonIdx = id.indexOf(':');
18
- const issuer = colonIdx > 0 ? id.slice(0, colonIdx) : id;
19
- const sub = colonIdx > 0 ? id.slice(colonIdx + 1) : null;
20
-
21
- if (sub) return `${sub}+${issuer}`;
22
- if (name) return `${name}+${issuer}`;
23
- return issuer;
24
- }
25
- </script>
26
-
27
- <span class="inline-flex shrink-0 rounded-full overflow-hidden" style="width: {size}px; height: {size}px">
28
- {@html svg}
29
- </span>
@@ -1,73 +0,0 @@
1
- <script lang="ts">
2
- import { Badge } from '@ui/components/badge/index';
3
- import { Separator } from '@ui/components/separator/index';
4
- import type { ChannelInfo } from '../api';
5
- import { formatRelative } from '../time';
6
-
7
- let {
8
- channel,
9
- viewMode = $bindable('pretty'),
10
- isAdmin = false,
11
- onMenuClick,
12
- onEditChannel,
13
- }: {
14
- channel: ChannelInfo;
15
- viewMode?: 'pretty' | 'raw';
16
- isAdmin?: boolean;
17
- onMenuClick?: () => void;
18
- onEditChannel?: () => void;
19
- } = $props();
20
- </script>
21
-
22
- <div class="flex items-center gap-3 px-4 h-12 border-b border-border shrink-0">
23
- {#if onMenuClick}
24
- <button
25
- onclick={onMenuClick}
26
- class="p-1 rounded hover:bg-secondary transition-colors md:hidden shrink-0"
27
- aria-label="Open channels"
28
- >
29
- <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>
30
- </button>
31
- {/if}
32
-
33
- <span class="text-muted-foreground/60">#</span>
34
- <span class="font-semibold text-sm">{channel.name}</span>
35
-
36
- {#if !channel.is_public}
37
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground/40"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
38
- {/if}
39
-
40
- {#if channel.description}
41
- <Separator orientation="vertical" class="h-4" />
42
- <span class="text-xs text-muted-foreground truncate hidden sm:block">{channel.description}</span>
43
- {/if}
44
-
45
- <div class="ml-auto flex items-center gap-2">
46
- {#if isAdmin && onEditChannel}
47
- <button
48
- onclick={onEditChannel}
49
- class="p-1 rounded hover:bg-secondary transition-colors text-muted-foreground hover:text-foreground"
50
- title="Edit channel"
51
- >
52
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
53
- </button>
54
- {/if}
55
- <span class="text-[10px] text-muted-foreground hidden sm:block">
56
- {channel.event_count} events
57
- </span>
58
- <div class="flex items-center bg-muted/50 rounded-md p-0.5 text-[11px]">
59
- <button
60
- class="px-2 py-0.5 rounded transition-colors {viewMode === 'pretty' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
61
- onclick={() => viewMode = 'pretty'}
62
- >
63
- Pretty
64
- </button>
65
- <button
66
- class="px-2 py-0.5 rounded transition-colors font-mono {viewMode === 'raw' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
67
- onclick={() => viewMode = 'raw'}
68
- >
69
- {'{ }'}
70
- </button>
71
- </div>
72
- </div>
73
- </div>