@webmcp-auto-ui/ui 2.5.31 → 2.5.33

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 (80) hide show
  1. package/package.json +15 -2
  2. package/src/agent/DiagnosticModal.svelte +126 -50
  3. package/src/agent/EphemeralBubble.svelte +13 -3
  4. package/src/agent/MCPserversList.svelte +147 -0
  5. package/src/agent/McpConnector.svelte +10 -1
  6. package/src/agent/RecipeBrowser.svelte +384 -0
  7. package/src/agent/RemoteMCPserversDemo.svelte +5 -121
  8. package/src/agent/ToolBrowser.svelte +133 -0
  9. package/src/agent/WebMCPserversList.svelte +2 -0
  10. package/src/agent/useAgentLoop.svelte.ts +396 -0
  11. package/src/base/chat-inline.svelte +64 -0
  12. package/src/base/dialog-content.svelte +3 -1
  13. package/src/components/HeaderControls.svelte +78 -0
  14. package/src/index.ts +13 -35
  15. package/src/stores/canvas.svelte.ts +0 -6
  16. package/src/widgets/SafeImage.svelte +67 -0
  17. package/src/widgets/WidgetRenderer.svelte +153 -78
  18. package/src/widgets/notebook/executors/index.ts +0 -1
  19. package/src/widgets/notebook/executors/sql.ts +32 -182
  20. package/src/widgets/notebook/import-modal-api.ts +237 -0
  21. package/src/widgets/notebook/import-modal.svelte +738 -0
  22. package/src/widgets/notebook/left-pane.ts +1 -1
  23. package/src/widgets/notebook/notebook.svelte +75 -0
  24. package/src/widgets/notebook/notebook.ts +38 -73
  25. package/src/widgets/notebook/prose.ts +6 -3
  26. package/src/widgets/notebook/shared.ts +68 -49
  27. package/src/widgets/rich/cards.svelte +74 -0
  28. package/src/widgets/rich/carousel.svelte +126 -0
  29. package/src/widgets/rich/chart-rich.svelte +221 -0
  30. package/src/widgets/rich/chat-input.svelte +52 -0
  31. package/src/widgets/rich/data-table.svelte +132 -0
  32. package/src/widgets/rich/gallery.svelte +115 -0
  33. package/src/widgets/rich/grid-data.svelte +85 -0
  34. package/src/widgets/rich/hemicycle.svelte +95 -0
  35. package/src/widgets/rich/js-sandbox.svelte +67 -0
  36. package/src/widgets/rich/json-viewer.svelte +82 -0
  37. package/src/widgets/rich/log.svelte +62 -0
  38. package/src/widgets/rich/profile.svelte +91 -0
  39. package/src/widgets/rich/sankey.svelte +73 -0
  40. package/src/widgets/rich/stat-card.svelte +60 -0
  41. package/src/widgets/rich/timeline.svelte +95 -0
  42. package/src/widgets/rich/trombinoscope.svelte +87 -0
  43. package/src/widgets/simple/actions.svelte +36 -0
  44. package/src/widgets/simple/alert.svelte +52 -0
  45. package/src/widgets/simple/chart.svelte +38 -0
  46. package/src/widgets/simple/code.svelte +30 -0
  47. package/src/widgets/simple/kv.svelte +31 -0
  48. package/src/widgets/simple/list.svelte +35 -0
  49. package/src/widgets/simple/stat.svelte +36 -0
  50. package/src/widgets/simple/tags.svelte +34 -0
  51. package/src/widgets/simple/text.svelte +130 -0
  52. package/src/widgets/helpers/safe-image.ts +0 -78
  53. package/src/widgets/notebook/import-modals.ts +0 -560
  54. package/src/widgets/notebook/recipe-browser.ts +0 -350
  55. package/src/widgets/rich/cards.ts +0 -181
  56. package/src/widgets/rich/carousel.ts +0 -319
  57. package/src/widgets/rich/chart-rich.ts +0 -386
  58. package/src/widgets/rich/d3.ts +0 -503
  59. package/src/widgets/rich/data-table.ts +0 -342
  60. package/src/widgets/rich/gallery.ts +0 -350
  61. package/src/widgets/rich/grid-data.ts +0 -173
  62. package/src/widgets/rich/hemicycle.ts +0 -313
  63. package/src/widgets/rich/js-sandbox.ts +0 -122
  64. package/src/widgets/rich/json-viewer.ts +0 -202
  65. package/src/widgets/rich/log.ts +0 -143
  66. package/src/widgets/rich/map.ts +0 -218
  67. package/src/widgets/rich/profile.ts +0 -256
  68. package/src/widgets/rich/sankey.ts +0 -257
  69. package/src/widgets/rich/stat-card.ts +0 -125
  70. package/src/widgets/rich/timeline.ts +0 -179
  71. package/src/widgets/rich/trombinoscope.ts +0 -246
  72. package/src/widgets/simple/actions.ts +0 -89
  73. package/src/widgets/simple/alert.ts +0 -100
  74. package/src/widgets/simple/chart.ts +0 -189
  75. package/src/widgets/simple/code.ts +0 -79
  76. package/src/widgets/simple/kv.ts +0 -68
  77. package/src/widgets/simple/list.ts +0 -89
  78. package/src/widgets/simple/stat.ts +0 -58
  79. package/src/widgets/simple/tags.ts +0 -125
  80. package/src/widgets/simple/text.ts +0 -198
@@ -0,0 +1,95 @@
1
+ <svelte:options customElement={{ tag: 'auto-timeline', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface TimelineEvent {
5
+ date?: string;
6
+ title?: string;
7
+ description?: string;
8
+ status?: 'done' | 'active' | 'pending';
9
+ color?: string;
10
+ href?: string;
11
+ tags?: string[];
12
+ }
13
+
14
+ export interface TimelineData {
15
+ title?: string;
16
+ events?: TimelineEvent[];
17
+ }
18
+
19
+ interface Props {
20
+ data?: TimelineData | null;
21
+ oneventclick?: (e: TimelineEvent) => void;
22
+ }
23
+
24
+ let { data = {}, oneventclick }: Props = $props();
25
+
26
+ const STATUS: Record<string, string> = {
27
+ done: 'var(--color-teal)',
28
+ active: 'var(--color-accent)',
29
+ pending: 'var(--color-border2)',
30
+ };
31
+
32
+ const events = $derived<TimelineEvent[]>(
33
+ Array.isArray(data?.events) && (data!.events as unknown[]).length
34
+ ? data!.events as TimelineEvent[]
35
+ : []
36
+ );
37
+
38
+ function handleEventActivate(event: TimelineEvent, e: KeyboardEvent) {
39
+ if (e.key === 'Enter' || e.key === ' ') {
40
+ e.preventDefault();
41
+ oneventclick?.(event);
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
47
+ {#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
48
+ {#if events.length === 0}
49
+ <p class="text-text2 text-sm">No events</p>
50
+ {:else}
51
+ <div class="py-1">
52
+ {#each events as event, i}
53
+ {@const isLast = i === events.length - 1}
54
+ {@const dotColor = event.color ?? STATUS[event.status ?? 'pending'] ?? 'var(--color-border2)'}
55
+ <div
56
+ class="flex gap-4 relative {!isLast ? 'pb-5' : ''} {oneventclick ? 'cursor-pointer' : ''}"
57
+ role={oneventclick ? 'button' : undefined}
58
+ tabindex={oneventclick ? 0 : undefined}
59
+ aria-label={oneventclick ? `${event.title ?? 'Event'} — ${event.date ?? ''}` : undefined}
60
+ title={oneventclick ? 'Double-cliquez pour interagir' : undefined}
61
+ ondblclick={() => oneventclick?.(event)}
62
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oneventclick?.(event); } }}
63
+ >
64
+ <div class="flex flex-col items-center flex-shrink-0">
65
+ <div
66
+ class="w-3 h-3 rounded-full flex-shrink-0 mt-0.5"
67
+ style="background:{dotColor};{event.status === 'active' ? `box-shadow:0 0 0 3px ${dotColor}33;` : ''}"
68
+ ></div>
69
+ {#if !isLast}<div class="w-0.5 flex-1 bg-border mt-1"></div>{/if}
70
+ </div>
71
+ <div class="flex-1 min-w-0 pb-1">
72
+ <div class="text-xs text-text2 mb-0.5">{event.date ?? ''}</div>
73
+ <div class="font-semibold text-text1 text-sm">
74
+ {#if event.href}
75
+ <a href={event.href} class="text-accent no-underline hover:underline">{event.title ?? ''}</a>
76
+ {:else}
77
+ {event.title ?? ''}
78
+ {/if}
79
+ </div>
80
+ {#if event.description}
81
+ <div class="text-sm text-text2 mt-0.5">{event.description}</div>
82
+ {/if}
83
+ {#if event.tags?.length}
84
+ <div class="flex gap-1 flex-wrap mt-1">
85
+ {#each event.tags as tag}
86
+ <span class="text-xs bg-surface2 text-text2 px-1.5 py-0.5 rounded">{tag}</span>
87
+ {/each}
88
+ </div>
89
+ {/if}
90
+ </div>
91
+ </div>
92
+ {/each}
93
+ </div>
94
+ {/if}
95
+ </div>
@@ -0,0 +1,87 @@
1
+ <svelte:options customElement={{ tag: 'auto-trombinoscope', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface TrombinoscopePerson { name: string; subtitle?: string; avatar?: string; badge?: string; color?: string; badgeColor?: string; }
5
+ export interface TrombinoscopeData {
6
+ title?: string;
7
+ people?: TrombinoscopePerson[];
8
+ columns?: number;
9
+ showBadge?: boolean;
10
+ }
11
+
12
+ interface Props {
13
+ data?: TrombinoscopeData | null;
14
+ onpersonclick?: (p: TrombinoscopePerson) => void;
15
+ }
16
+ let { data = {}, onpersonclick }: Props = $props();
17
+
18
+ const COLORS = ['#7c6dfa', '#3ecfb2', '#f0a050', '#fa6d7c', '#3b82f6', '#a855f7', '#14b8a6', '#f97316'];
19
+ const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
20
+
21
+ function isValidImageUrl(url: string | undefined): boolean {
22
+ return !!url && VALID_PREFIXES.some(p => url.startsWith(p));
23
+ }
24
+ function nameColor(name: string): string {
25
+ let h = 0;
26
+ for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h);
27
+ return COLORS[Math.abs(h) % COLORS.length];
28
+ }
29
+ function initials(name: string): string {
30
+ return name.split(/\s+/).slice(0, 2).map((w: string) => w[0] ?? '').join('').toUpperCase() || '?';
31
+ }
32
+
33
+ /** Track per-person avatar load failures to fall back to initials */
34
+ let failedAvatars = $state(new Set<string>());
35
+
36
+ const people = $derived.by<TrombinoscopePerson[]>(() => {
37
+ if (Array.isArray(data?.people) && data!.people!.length) return data!.people!;
38
+ return [];
39
+ });
40
+
41
+ const cols = $derived(data?.columns ?? 4);
42
+ </script>
43
+
44
+ <div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
45
+ {#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
46
+ {#if people.length === 0}<p class="text-text2 text-sm">Aucune personne</p>
47
+ {:else}
48
+ <div class="grid gap-3 responsive-trombi" style="--trombi-cols: repeat({cols}, minmax(0, 1fr));">
49
+ {#each people as person}
50
+ {@const accent = person.color ?? nameColor(person.name)}
51
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
52
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
53
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
54
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
55
+ <div
56
+ class="flex flex-col items-center text-center p-3 rounded-lg border border-border hover:border-border2 transition-all {onpersonclick ? 'cursor-pointer' : ''}"
57
+ role={onpersonclick ? 'button' : undefined}
58
+ tabindex={onpersonclick ? 0 : undefined}
59
+ onclick={() => onpersonclick?.(person)}
60
+ >
61
+ {#if isValidImageUrl(person.avatar) && !failedAvatars.has(person.avatar!)}
62
+ <img
63
+ src={person.avatar}
64
+ alt={person.name}
65
+ class="w-12 h-12 rounded-full object-cover mb-2 border-2"
66
+ style="border-color:{accent};"
67
+ onerror={() => { failedAvatars = new Set([...failedAvatars, person.avatar!]); }}
68
+ />
69
+ {:else}
70
+ <div class="w-12 h-12 rounded-full flex items-center justify-center text-white font-bold text-base mb-2 flex-shrink-0" style="background:{accent};">{initials(person.name)}</div>
71
+ {/if}
72
+ <div class="text-xs font-semibold text-text1 leading-tight truncate w-full">{person.name}</div>
73
+ {#if person.subtitle}<div class="text-xs text-text2 mt-0.5 truncate w-full">{person.subtitle}</div>{/if}
74
+ {#if data?.showBadge !== false && person.badge}
75
+ <span class="text-xs font-semibold px-2 py-0.5 rounded-full mt-1.5 text-white" style="background:{person.badgeColor ?? accent};">{person.badge}</span>
76
+ {/if}
77
+ </div>
78
+ {/each}
79
+ </div>
80
+ <div class="mt-3 text-xs text-text2">{people.length} personne{people.length !== 1 ? 's' : ''}</div>
81
+ {/if}
82
+ </div>
83
+
84
+ <style>
85
+ .responsive-trombi { grid-template-columns: repeat(2, minmax(0, 1fr)); }
86
+ @media (min-width: 768px) { .responsive-trombi { grid-template-columns: var(--trombi-cols); } }
87
+ </style>
@@ -0,0 +1,36 @@
1
+ <svelte:options customElement={{ tag: 'auto-actions', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface ActionButton {
5
+ label: string;
6
+ primary?: boolean;
7
+ /** aria-label override for icon-only buttons */
8
+ ariaLabel?: string;
9
+ onclick?: () => void;
10
+ }
11
+
12
+ export interface ActionsData {
13
+ buttons?: ActionButton[];
14
+ }
15
+
16
+ interface Props {
17
+ data?: ActionsData | null;
18
+ }
19
+
20
+ let { data = {} }: Props = $props();
21
+
22
+ const buttons = $derived(data?.buttons ?? []);
23
+ </script>
24
+
25
+ <div class="p-3 md:p-4 flex gap-2 flex-wrap">
26
+ {#each buttons as btn}
27
+ <button
28
+ class="text-xs font-mono px-4 py-2 rounded border transition-all
29
+ {btn.primary
30
+ ? 'bg-accent border-accent text-white hover:opacity-85'
31
+ : 'border-border2 text-text2 hover:border-accent hover:text-accent'}"
32
+ aria-label={btn.ariaLabel ?? btn.label}
33
+ onclick={btn.onclick}
34
+ >{btn.label}</button>
35
+ {/each}
36
+ </div>
@@ -0,0 +1,52 @@
1
+ <svelte:options customElement={{ tag: 'auto-alert', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface AlertData {
5
+ title?: string;
6
+ message?: string;
7
+ level?: 'info' | 'warn' | 'error';
8
+ }
9
+
10
+ interface Props {
11
+ data?: AlertData | null;
12
+ }
13
+
14
+ let { data = {} }: Props = $props();
15
+
16
+ const title = $derived(data?.title);
17
+ const message = $derived(data?.message);
18
+ const level = $derived(data?.level ?? 'warn');
19
+
20
+ const borderColor = $derived(
21
+ level === 'error'
22
+ ? 'border-accent2'
23
+ : level === 'info'
24
+ ? 'border-blue-500'
25
+ : 'border-amber',
26
+ );
27
+ const titleColor = $derived(
28
+ level === 'error'
29
+ ? 'text-accent2'
30
+ : level === 'info'
31
+ ? 'text-blue-400'
32
+ : 'text-amber',
33
+ );
34
+
35
+ // a11y: error/warn → role="alert" + aria-live="assertive"
36
+ // info → role="status" + aria-live="polite"
37
+ const role = $derived(level === 'error' || level === 'warn' ? 'alert' : 'status');
38
+ const ariaLive = $derived(level === 'error' || level === 'warn' ? 'assertive' : 'polite');
39
+ </script>
40
+
41
+ <div
42
+ class="p-3 md:p-4 border-l-4 {borderColor}"
43
+ role={role}
44
+ aria-live={ariaLive}
45
+ >
46
+ {#if title}
47
+ <div class="font-semibold text-sm mb-1 {titleColor}">{title}</div>
48
+ {/if}
49
+ {#if message}
50
+ <div class="text-xs font-mono text-text2">{message}</div>
51
+ {/if}
52
+ </div>
@@ -0,0 +1,38 @@
1
+ <svelte:options customElement={{ tag: 'auto-chart', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface ChartData {
5
+ title?: string;
6
+ bars?: [string, number][];
7
+ }
8
+
9
+ interface Props {
10
+ data?: ChartData | null;
11
+ }
12
+
13
+ let { data = {} }: Props = $props();
14
+
15
+ const title = $derived(data?.title);
16
+ const bars = $derived(data?.bars ?? []);
17
+ const max = $derived(Math.max(...bars.map((b) => b[1]), 1));
18
+ </script>
19
+
20
+ <div class="p-3 md:p-4">
21
+ {#if title}
22
+ <div class="text-[10px] font-mono text-text2 mb-4 uppercase tracking-widest">{title}</div>
23
+ {/if}
24
+ <div class="flex items-end gap-1.5 h-32" role="img" aria-label={title ?? 'Bar chart'}>
25
+ {#each bars as [label, val]}
26
+ <div
27
+ class="flex-1 rounded-t bg-accent opacity-80 hover:opacity-100 transition-all"
28
+ style="height: max(2px, {Math.round((val / max) * 100)}%)"
29
+ title="{label}: {val}"
30
+ ></div>
31
+ {/each}
32
+ </div>
33
+ <div class="flex gap-1.5 mt-1" aria-hidden="true">
34
+ {#each bars as [label]}
35
+ <span class="flex-1 text-center text-[9px] font-mono text-text2 truncate">{label}</span>
36
+ {/each}
37
+ </div>
38
+ </div>
@@ -0,0 +1,30 @@
1
+ <svelte:options customElement={{ tag: 'auto-code', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface CodeData {
5
+ lang?: string;
6
+ content?: string;
7
+ }
8
+
9
+ interface Props {
10
+ data?: CodeData | null;
11
+ }
12
+
13
+ let { data = {} }: Props = $props();
14
+
15
+ const lang = $derived(data?.lang ?? 'text');
16
+ const content = $derived(data?.content ?? '');
17
+ </script>
18
+
19
+ <div class="rounded overflow-hidden">
20
+ <div class="bg-black/40 px-3 py-1.5 md:px-4 border-b border-border">
21
+ <span class="text-[10px] font-mono text-text2">{lang}</span>
22
+ </div>
23
+ <!-- a11y: tabindex + focus-visible for keyboard access to scrollable code block -->
24
+ <pre
25
+ class="font-mono text-xs text-teal bg-black/30 p-3 md:p-4 overflow-x-auto leading-relaxed focus:outline focus:outline-2 focus:outline-accent"
26
+ tabindex="0"
27
+ role="region"
28
+ aria-label="Code block: {lang}"
29
+ >{content}</pre>
30
+ </div>
@@ -0,0 +1,31 @@
1
+ <svelte:options customElement={{ tag: 'auto-kv', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface KvData {
5
+ title?: string;
6
+ rows?: [string, string][];
7
+ }
8
+
9
+ interface Props {
10
+ data?: KvData | null;
11
+ }
12
+
13
+ let { data = {} }: Props = $props();
14
+
15
+ const title = $derived(data?.title);
16
+ const rows = $derived(data?.rows ?? []);
17
+ </script>
18
+
19
+ <div class="p-3 md:p-4">
20
+ {#if title}
21
+ <div class="text-[10px] font-mono text-text2 mb-3 uppercase tracking-widest">{title}</div>
22
+ {/if}
23
+ <div class="flex flex-col gap-1.5">
24
+ {#each rows as [k, v]}
25
+ <div class="flex justify-between items-center text-sm border-b border-border pb-1.5 last:border-none last:pb-0">
26
+ <span class="font-mono text-xs text-text2">{k}</span>
27
+ <span class="text-text1 font-medium">{v}</span>
28
+ </div>
29
+ {/each}
30
+ </div>
31
+ </div>
@@ -0,0 +1,35 @@
1
+ <svelte:options customElement={{ tag: 'auto-list', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface ListData {
5
+ title?: string;
6
+ items?: string[];
7
+ }
8
+
9
+ interface Props {
10
+ data?: ListData | null;
11
+ onitemclick?: (item: string, index: number) => void;
12
+ }
13
+
14
+ let { data = {}, onitemclick }: Props = $props();
15
+
16
+ const title = $derived(data?.title);
17
+ const items = $derived(data?.items ?? []);
18
+ </script>
19
+
20
+ <div class="p-3 md:p-4">
21
+ {#if title}
22
+ <div class="text-[10px] font-mono text-text2 mb-3 uppercase tracking-widest">{title}</div>
23
+ {/if}
24
+ <ul class="flex flex-col gap-1.5">
25
+ {#each items as item, i}
26
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
27
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
28
+ <li
29
+ class="text-sm text-text1 bg-surface2 rounded px-3 py-2 border-l-2 border-accent {onitemclick ? 'cursor-pointer hover:bg-surface2/80' : ''}"
30
+ title={onitemclick ? 'Double-cliquez pour interagir' : undefined}
31
+ ondblclick={() => onitemclick?.(item, i)}
32
+ >{item}</li>
33
+ {/each}
34
+ </ul>
35
+ </div>
@@ -0,0 +1,36 @@
1
+ <svelte:options customElement={{ tag: 'auto-stat', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface StatData {
5
+ label?: string;
6
+ value?: string | number;
7
+ trend?: string;
8
+ trendDir?: 'up' | 'down' | 'neutral';
9
+ }
10
+
11
+ interface Props {
12
+ data?: StatData | null;
13
+ }
14
+
15
+ let { data = {} }: Props = $props();
16
+
17
+ const label = $derived(data?.label ?? 'Metric');
18
+ const value = $derived(data?.value ?? '—');
19
+ const trend = $derived(data?.trend);
20
+ const trendDir = $derived(data?.trendDir);
21
+
22
+ const trendColor = $derived(
23
+ trendDir === 'up' ? 'text-teal' : trendDir === 'down' ? 'text-accent2' : 'text-text2',
24
+ );
25
+ const trendArrow = $derived(
26
+ trendDir === 'up' ? '↑' : trendDir === 'down' ? '↓' : '→',
27
+ );
28
+ </script>
29
+
30
+ <div class="p-4 md:p-5">
31
+ <div class="text-[11px] font-mono text-text2 mb-1 uppercase tracking-widest">{label}</div>
32
+ <div class="text-3xl md:text-4xl font-bold text-text1 leading-none">{value}</div>
33
+ {#if trend}
34
+ <div class="text-xs font-mono mt-2 {trendColor}">{trendArrow} {trend}</div>
35
+ {/if}
36
+ </div>
@@ -0,0 +1,34 @@
1
+ <svelte:options customElement={{ tag: 'auto-tags', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface TagItem {
5
+ text: string;
6
+ active?: boolean;
7
+ }
8
+
9
+ export interface TagsData {
10
+ label?: string;
11
+ tags?: TagItem[];
12
+ }
13
+
14
+ interface Props {
15
+ data?: TagsData | null;
16
+ }
17
+
18
+ let { data = {} }: Props = $props();
19
+
20
+ const label = $derived(data?.label);
21
+ const tags = $derived(data?.tags ?? []);
22
+ </script>
23
+
24
+ <div class="p-3 md:p-4 flex gap-2 flex-wrap items-center">
25
+ {#if label}
26
+ <span class="text-[10px] font-mono text-text2">{label}</span>
27
+ {/if}
28
+ {#each tags as tag}
29
+ <span
30
+ class="text-[11px] font-mono px-3 py-1 rounded-full border transition-colors
31
+ {tag.active ? 'border-teal text-teal bg-teal/10' : 'border-border2 text-text2'}"
32
+ >{tag.text}</span>
33
+ {/each}
34
+ </div>
@@ -0,0 +1,130 @@
1
+ <svelte:options customElement={{ tag: 'auto-text', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ export interface TextData {
5
+ content?: string;
6
+ }
7
+
8
+ interface Props {
9
+ data?: TextData | null;
10
+ }
11
+
12
+ let { data = {} }: Props = $props();
13
+
14
+ /** Minimal markdown → HTML renderer (no deps, naturally XSS-safe: only produces known tags) */
15
+ function renderMarkdown(src: string): string {
16
+ if (!src) return '';
17
+
18
+ // Escape HTML entities first (XSS protection)
19
+ const esc = (s: string) =>
20
+ s
21
+ .replace(/&/g, '&amp;')
22
+ .replace(/</g, '&lt;')
23
+ .replace(/>/g, '&gt;')
24
+ .replace(/"/g, '&quot;');
25
+
26
+ const lines = src.split('\n');
27
+ const out: string[] = [];
28
+ let inCode = false;
29
+ let codeLines: string[] = [];
30
+ let inUl = false;
31
+ let inOl = false;
32
+
33
+ const closeList = () => {
34
+ if (inUl) { out.push('</ul>'); inUl = false; }
35
+ if (inOl) { out.push('</ol>'); inOl = false; }
36
+ };
37
+
38
+ /** Inline formatting: bold, italic, code, links */
39
+ const inline = (s: string): string =>
40
+ esc(s)
41
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
42
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
43
+ .replace(/\*([^*]+)\*/g, '<em>$1</em>')
44
+ .replace(
45
+ /\[([^\]]+)\]\(([^)]+)\)/g,
46
+ '<a href="$2" target="_blank" rel="noopener">$1</a>',
47
+ );
48
+
49
+ for (const line of lines) {
50
+ if (line.trimStart().startsWith('```')) {
51
+ if (!inCode) {
52
+ closeList();
53
+ inCode = true;
54
+ codeLines = [];
55
+ } else {
56
+ out.push(`<pre><code>${esc(codeLines.join('\n'))}</code></pre>`);
57
+ inCode = false;
58
+ }
59
+ continue;
60
+ }
61
+ if (inCode) { codeLines.push(line); continue; }
62
+
63
+ const trimmed = line.trim();
64
+
65
+ if (!trimmed) { closeList(); out.push(''); continue; }
66
+
67
+ const hMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
68
+ if (hMatch) {
69
+ closeList();
70
+ const level = hMatch[1].length;
71
+ out.push(`<h${level}>${inline(hMatch[2])}</h${level}>`);
72
+ continue;
73
+ }
74
+
75
+ if (/^[-*+]\s+/.test(trimmed)) {
76
+ if (inOl) { out.push('</ol>'); inOl = false; }
77
+ if (!inUl) { out.push('<ul>'); inUl = true; }
78
+ out.push(`<li>${inline(trimmed.replace(/^[-*+]\s+/, ''))}</li>`);
79
+ continue;
80
+ }
81
+
82
+ const olMatch = trimmed.match(/^(\d+)\.\s+(.+)$/);
83
+ if (olMatch) {
84
+ if (inUl) { out.push('</ul>'); inUl = false; }
85
+ if (!inOl) { out.push('<ol>'); inOl = true; }
86
+ out.push(`<li>${inline(olMatch[2])}</li>`);
87
+ continue;
88
+ }
89
+
90
+ if (/^[-*_]{3,}$/.test(trimmed)) {
91
+ closeList();
92
+ out.push('<hr>');
93
+ continue;
94
+ }
95
+
96
+ closeList();
97
+ out.push(`<p>${inline(trimmed)}</p>`);
98
+ }
99
+
100
+ if (inCode) out.push(`<pre><code>${esc(codeLines.join('\n'))}</code></pre>`);
101
+ closeList();
102
+
103
+ return out.join('\n');
104
+ }
105
+
106
+ const rendered = $derived(renderMarkdown(data?.content ?? ''));
107
+ </script>
108
+
109
+ <div class="tb-md p-4 md:p-5 text-sm leading-relaxed">{@html rendered}</div>
110
+
111
+ <style>
112
+ .tb-md { color: var(--color-text2); }
113
+ .tb-md :global(h1) { font-size: 1.5em; font-weight: 700; color: var(--color-text1); margin: 0.8em 0 0.4em; }
114
+ .tb-md :global(h2) { font-size: 1.25em; font-weight: 600; color: var(--color-text1); margin: 0.7em 0 0.35em; }
115
+ .tb-md :global(h3) { font-size: 1.1em; font-weight: 600; color: var(--color-text1); margin: 0.6em 0 0.3em; }
116
+ .tb-md :global(h4), .tb-md :global(h5), .tb-md :global(h6) { font-size: 1em; font-weight: 600; color: var(--color-text1); margin: 0.5em 0 0.25em; }
117
+ .tb-md :global(p) { margin: 0.4em 0; }
118
+ .tb-md :global(strong) { font-weight: 600; color: var(--color-text1); }
119
+ .tb-md :global(em) { font-style: italic; }
120
+ .tb-md :global(a) { color: var(--color-accent); text-decoration: underline; text-underline-offset: 2px; }
121
+ .tb-md :global(a:hover) { opacity: 0.8; }
122
+ .tb-md :global(ul), .tb-md :global(ol) { margin: 0.4em 0; padding-left: 1.5em; }
123
+ .tb-md :global(ul) { list-style: disc; }
124
+ .tb-md :global(ol) { list-style: decimal; }
125
+ .tb-md :global(li) { margin: 0.15em 0; }
126
+ .tb-md :global(code) { font-family: 'IBM Plex Mono', ui-monospace, monospace; font-size: 0.9em; background: var(--color-surface2); padding: 0.15em 0.35em; border-radius: 4px; }
127
+ .tb-md :global(pre) { background: var(--color-surface2); border-radius: 6px; padding: 0.75em 1em; margin: 0.5em 0; overflow-x: auto; }
128
+ .tb-md :global(pre code) { background: none; padding: 0; font-size: 0.85em; }
129
+ .tb-md :global(hr) { border: none; border-top: 1px solid var(--color-surface2); margin: 0.8em 0; }
130
+ </style>