@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
@@ -1,141 +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 } 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}
@@ -1,130 +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 } 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>
@@ -1,144 +0,0 @@
1
- <script lang="ts">
2
- import type { ChannelInfo } from '../api';
3
- import AdminDropdown from './admin-dropdown.svelte';
4
-
5
- import type { Component } from 'svelte';
6
-
7
- interface SettingsPageExtension {
8
- slug: string;
9
- label: string;
10
- icon?: Component;
11
- }
12
-
13
- let {
14
- channels,
15
- selectedId,
16
- serverName,
17
- hasAuth,
18
- isAdmin,
19
- status,
20
- pollInterval,
21
- onSelect,
22
- onAuthClick,
23
- onClose,
24
- onServerConfig,
25
- onKeysAndTokens,
26
- onCreateChannel,
27
- extensionSettingsPages = [],
28
- onExtensionSettings,
29
- }: {
30
- channels: ChannelInfo[];
31
- selectedId: string | null;
32
- serverName: string;
33
- hasAuth: boolean;
34
- isAdmin: boolean;
35
- status: 'connected' | 'polling' | 'reconnecting' | 'error' | 'idle' | 'loading';
36
- pollInterval: number;
37
- onSelect: (id: string) => void;
38
- onAuthClick: () => void;
39
- onClose?: () => void;
40
- onServerConfig: () => void;
41
- onKeysAndTokens: () => void;
42
- onCreateChannel: () => void;
43
- extensionSettingsPages?: SettingsPageExtension[];
44
- onExtensionSettings?: (slug: string) => void;
45
- } = $props();
46
-
47
- const statusColor: Record<string, string> = {
48
- connected: 'bg-primary',
49
- polling: 'bg-primary',
50
- reconnecting: 'bg-yellow-500',
51
- error: 'bg-destructive',
52
- idle: 'bg-muted-foreground',
53
- loading: 'bg-muted-foreground',
54
- };
55
-
56
- const statusLabel: Record<string, string> = {
57
- connected: 'Connected',
58
- polling: 'Connected',
59
- reconnecting: 'Reconnecting...',
60
- error: 'Error',
61
- idle: 'Idle',
62
- loading: 'Loading...',
63
- };
64
- </script>
65
-
66
- <div class="flex flex-col h-full w-full bg-[oklch(0.13_0_0)] border-r border-border">
67
- <!-- Server header -->
68
- <div class="flex items-center justify-between px-3 h-12 border-b border-border shrink-0">
69
- {#if isAdmin}
70
- <AdminDropdown {serverName} {onServerConfig} {onKeysAndTokens} {extensionSettingsPages} {onExtensionSettings} />
71
- {:else}
72
- <span class="font-semibold text-sm truncate">{serverName}</span>
73
- {/if}
74
- {#if onClose}
75
- <button
76
- onclick={onClose}
77
- class="p-1.5 rounded hover:bg-secondary transition-colors md:hidden"
78
- aria-label="Close sidebar"
79
- >
80
- <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>
81
- </button>
82
- {/if}
83
- </div>
84
-
85
- <!-- Channel list -->
86
- <div class="flex-1 overflow-y-auto py-2">
87
- <div class="px-3 mb-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">Channels</div>
88
- {#each channels as ch (ch.id)}
89
- <button
90
- class="w-full text-left px-3 py-1.5 flex items-center gap-2 text-sm transition-colors
91
- {selectedId === ch.id ? 'bg-secondary text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-secondary/50'}"
92
- onclick={() => onSelect(ch.id)}
93
- >
94
- <span class="text-muted-foreground/60 shrink-0">#</span>
95
- <span class="truncate flex-1">{ch.name}</span>
96
- {#if !ch.is_public}
97
- <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="text-muted-foreground/40 shrink-0"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
98
- {/if}
99
- </button>
100
- {/each}
101
-
102
- {#if channels.length === 0 && !isAdmin}
103
- <div class="px-3 py-4 text-xs text-muted-foreground/60 text-center">
104
- No channels yet
105
- </div>
106
- {/if}
107
-
108
- {#if isAdmin}
109
- <button
110
- class="w-full text-left px-3 py-1.5 flex items-center gap-2 text-sm text-muted-foreground/50 hover:text-muted-foreground hover:bg-secondary/50 transition-colors"
111
- onclick={onCreateChannel}
112
- >
113
- <span class="shrink-0">+</span>
114
- <span>Create channel</span>
115
- </button>
116
- {/if}
117
- </div>
118
-
119
- <!-- Profile -->
120
- <button
121
- onclick={onAuthClick}
122
- class="border-t border-border px-3 h-12 flex items-center gap-2 w-full hover:bg-secondary/50 transition-colors shrink-0"
123
- title={hasAuth ? 'Authenticated' : 'Sign in'}
124
- >
125
- <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" class={hasAuth ? 'text-primary' : 'text-muted-foreground'}>
126
- <circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/>
127
- </svg>
128
- <span class="text-xs {hasAuth ? 'text-foreground' : 'text-muted-foreground'}">{hasAuth ? 'Signed in' : 'Sign in'}</span>
129
- </button>
130
-
131
- <!-- Status + powered by -->
132
- <div class="border-t border-border px-3 py-2 flex items-center justify-between text-[10px] text-muted-foreground/50">
133
- <div class="flex items-center gap-1.5">
134
- <span class={`inline-block w-1.5 h-1.5 rounded-full ${statusColor[status]}`}></span>
135
- <span>{statusLabel[status]}</span>
136
- {#if status === 'connected'}
137
- <span class="text-muted-foreground/30">WS</span>
138
- {:else if status === 'polling'}
139
- <span class="text-muted-foreground/30">{pollInterval}s</span>
140
- {/if}
141
- </div>
142
- <a href="https://zooid.dev" class="underline hover:text-muted-foreground/70">Zooid</a>
143
- </div>
144
- </div>
@@ -1,40 +0,0 @@
1
- <script lang="ts">
2
- let {
3
- status,
4
- pollInterval,
5
- }: {
6
- status: 'connected' | 'polling' | 'reconnecting' | 'error' | 'idle' | 'loading';
7
- pollInterval: number;
8
- } = $props();
9
-
10
- const statusText: Record<string, string> = {
11
- connected: 'Connected',
12
- polling: 'Connected',
13
- reconnecting: 'Reconnecting...',
14
- error: 'Error',
15
- idle: 'Idle',
16
- loading: 'Loading...',
17
- };
18
-
19
- const statusColor: Record<string, string> = {
20
- connected: 'bg-primary',
21
- polling: 'bg-primary',
22
- reconnecting: 'bg-yellow-500',
23
- error: 'bg-destructive',
24
- idle: 'bg-muted-foreground',
25
- loading: 'bg-muted-foreground',
26
- };
27
- </script>
28
-
29
- <div class="flex items-center justify-between px-4 py-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))] text-xs text-muted-foreground border-t border-border">
30
- <div class="flex items-center gap-2">
31
- <span class={`inline-block w-1.5 h-1.5 rounded-full ${statusColor[status]}`}></span>
32
- <span>{statusText[status]}</span>
33
- {#if status === 'connected'}
34
- <span class="text-muted-foreground/60">WebSocket</span>
35
- {:else if status === 'polling'}
36
- <span class="text-muted-foreground/60">poll every {pollInterval}s</span>
37
- {/if}
38
- </div>
39
- <span class="text-muted-foreground/40"><a href="https://zooid.dev" class="underline hover:text-muted-foreground">Powered by Zooid</a></span>
40
- </div>
@@ -1,200 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { looksLikeMarkdown, parsePretty, renderMarkdown } from './pretty-json';
3
-
4
- describe('parsePretty', () => {
5
- it('parses flat object into text nodes', () => {
6
- const raw = JSON.stringify({ title: 'Hello', count: 42 });
7
- const nodes = parsePretty(raw);
8
- expect(nodes).toEqual([
9
- {
10
- kind: 'text',
11
- key: 'title',
12
- value: 'Hello',
13
- markdown: false,
14
- multiline: false,
15
- },
16
- {
17
- kind: 'text',
18
- key: 'count',
19
- value: '42',
20
- markdown: false,
21
- multiline: false,
22
- },
23
- ]);
24
- });
25
-
26
- it('preserves newlines in string values and marks as multiline', () => {
27
- const raw = JSON.stringify({ body: 'line one\nline two\nline three' });
28
- const nodes = parsePretty(raw)!;
29
- expect(nodes[0].kind).toBe('text');
30
- const text = nodes[0] as Extract<(typeof nodes)[0], { kind: 'text' }>;
31
- expect(text.value).toBe('line one\nline two\nline three');
32
- expect(text.multiline).toBe(true);
33
- expect(text.value.split('\n')).toEqual([
34
- 'line one',
35
- 'line two',
36
- 'line three',
37
- ]);
38
- });
39
-
40
- it('marks single-line strings as not multiline', () => {
41
- const raw = JSON.stringify({ title: 'no newlines here' });
42
- const nodes = parsePretty(raw)!;
43
- const text = nodes[0] as Extract<(typeof nodes)[0], { kind: 'text' }>;
44
- expect(text.multiline).toBe(false);
45
- });
46
-
47
- it('parses nested object as group node', () => {
48
- const raw = JSON.stringify({ meta: { author: 'bot', version: 1 } });
49
- const nodes = parsePretty(raw)!;
50
- expect(nodes).toHaveLength(1);
51
- expect(nodes[0].kind).toBe('group');
52
- const group = nodes[0] as Extract<(typeof nodes)[0], { kind: 'group' }>;
53
- expect(group.key).toBe('meta');
54
- expect(group.children).toEqual([
55
- {
56
- kind: 'text',
57
- key: 'author',
58
- value: 'bot',
59
- markdown: false,
60
- multiline: false,
61
- },
62
- {
63
- kind: 'text',
64
- key: 'version',
65
- value: '1',
66
- markdown: false,
67
- multiline: false,
68
- },
69
- ]);
70
- });
71
-
72
- it('parses array of objects as list node', () => {
73
- const raw = JSON.stringify({
74
- posts: [
75
- { title: 'A', score: 10 },
76
- { title: 'B', score: 20 },
77
- ],
78
- });
79
- const nodes = parsePretty(raw)!;
80
- expect(nodes).toHaveLength(1);
81
- expect(nodes[0].kind).toBe('list');
82
- const list = nodes[0] as Extract<(typeof nodes)[0], { kind: 'list' }>;
83
- expect(list.items).toHaveLength(2);
84
- expect(list.items[0][0]).toEqual({
85
- kind: 'text',
86
- key: 'title',
87
- value: 'A',
88
- markdown: false,
89
- multiline: false,
90
- });
91
- });
92
-
93
- it('detects markdown in string values', () => {
94
- const raw = JSON.stringify({ summary: '**bold** and *italic*' });
95
- const nodes = parsePretty(raw)!;
96
- const text = nodes[0] as Extract<(typeof nodes)[0], { kind: 'text' }>;
97
- expect(text.markdown).toBe(true);
98
- });
99
-
100
- it('returns null for non-object JSON', () => {
101
- expect(parsePretty('"hello"')).toBeNull();
102
- expect(parsePretty('[1,2,3]')).toBeNull();
103
- expect(parsePretty('42')).toBeNull();
104
- });
105
-
106
- it('returns null for invalid JSON', () => {
107
- expect(parsePretty('not json')).toBeNull();
108
- });
109
- });
110
-
111
- describe('ref convention', () => {
112
- it('should mark ref field with kind "ref"', () => {
113
- const nodes = parsePretty(
114
- JSON.stringify({
115
- body: 'Trade executed',
116
- ref: 'zooid:signals/01HWXYZ123456789012345A',
117
- }),
118
- );
119
-
120
- const refNode = nodes!.find((n) => n.key === 'ref');
121
- expect(refNode).toBeDefined();
122
- expect(refNode!.kind).toBe('ref');
123
- expect((refNode as { value: string }).value).toBe(
124
- 'zooid:signals/01HWXYZ123456789012345A',
125
- );
126
- });
127
-
128
- it('should mark ref with https: scheme', () => {
129
- const nodes = parsePretty(
130
- JSON.stringify({
131
- ref: 'https://example.com/resource',
132
- }),
133
- );
134
-
135
- const refNode = nodes!.find((n) => n.key === 'ref');
136
- expect(refNode!.kind).toBe('ref');
137
- });
138
-
139
- it('should not mark non-ref string fields as ref', () => {
140
- const nodes = parsePretty(
141
- JSON.stringify({
142
- body: 'zooid:signals/01HWXYZ123456789012345A',
143
- symbol: 'AAPL',
144
- }),
145
- );
146
-
147
- const bodyNode = nodes!.find((n) => n.key === 'body');
148
- expect(bodyNode!.kind).toBe('text');
149
- });
150
- });
151
-
152
- describe('looksLikeMarkdown', () => {
153
- it('detects bold syntax', () => {
154
- expect(looksLikeMarkdown('some **bold** text')).toBe(true);
155
- });
156
-
157
- it('detects italic syntax', () => {
158
- expect(looksLikeMarkdown('some *italic* text')).toBe(true);
159
- });
160
-
161
- it('detects heading syntax', () => {
162
- expect(looksLikeMarkdown('# Heading')).toBe(true);
163
- });
164
-
165
- it('detects link syntax', () => {
166
- expect(looksLikeMarkdown('see [link](url)')).toBe(true);
167
- });
168
-
169
- it('detects code backticks', () => {
170
- expect(looksLikeMarkdown('use `code` here')).toBe(true);
171
- });
172
-
173
- it('does not flag plain text', () => {
174
- expect(looksLikeMarkdown('just a normal sentence')).toBe(false);
175
- });
176
-
177
- it('does not flag URLs without markdown', () => {
178
- expect(looksLikeMarkdown('https://example.com/path/to/page')).toBe(false);
179
- });
180
- });
181
-
182
- describe('renderMarkdown', () => {
183
- it('renders bold text', () => {
184
- const html = renderMarkdown('**bold**');
185
- expect(html).toContain('<strong>bold</strong>');
186
- });
187
-
188
- it('renders bullet lists', () => {
189
- const html = renderMarkdown('* item one\n* item two');
190
- expect(html).toContain('<li>');
191
- expect(html).toContain('item one');
192
- expect(html).toContain('item two');
193
- });
194
-
195
- it('renders links', () => {
196
- const html = renderMarkdown('[click](https://example.com)');
197
- expect(html).toContain('<a');
198
- expect(html).toContain('https://example.com');
199
- });
200
- });