@mcp-html-bridge/ui-engine 0.1.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.
@@ -0,0 +1,114 @@
1
+ // ── Reading Block Renderer: long text/markdown → formatted block ──
2
+ import { escapeHtml } from '../html-builder.js';
3
+
4
+ /** Simple markdown-like formatting (no dependencies) */
5
+ function formatText(text: string): string {
6
+ let html = escapeHtml(text);
7
+
8
+ // Headers
9
+ html = html.replace(/^### (.+)$/gm, '<h3 class="rb-h3">$1</h3>');
10
+ html = html.replace(/^## (.+)$/gm, '<h2 class="rb-h2">$1</h2>');
11
+ html = html.replace(/^# (.+)$/gm, '<h1 class="rb-h1">$1</h1>');
12
+
13
+ // Bold and italic
14
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
15
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
16
+
17
+ // Inline code
18
+ html = html.replace(/`([^`]+)`/g, '<code class="rb-code">$1</code>');
19
+
20
+ // Code blocks
21
+ html = html.replace(/```[\s\S]*?```/g, (match) => {
22
+ const code = match.slice(3, -3).replace(/^\w*\n/, '');
23
+ return `<pre class="rb-pre"><code>${code}</code></pre>`;
24
+ });
25
+
26
+ // Line breaks → paragraphs
27
+ html = html
28
+ .split(/\n\n+/)
29
+ .map((p) => {
30
+ const trimmed = p.trim();
31
+ if (!trimmed) return '';
32
+ if (trimmed.startsWith('<h') || trimmed.startsWith('<pre')) return trimmed;
33
+ return `<p>${trimmed.replace(/\n/g, '<br>')}</p>`;
34
+ })
35
+ .filter(Boolean)
36
+ .join('\n');
37
+
38
+ return html;
39
+ }
40
+
41
+ export function renderReadingBlock(
42
+ data: unknown,
43
+ metadata: Record<string, unknown>
44
+ ): string {
45
+ const textKeys = metadata['textKeys'] as string[] | undefined;
46
+
47
+ // Direct string
48
+ if (typeof data === 'string') {
49
+ return `<article class="reading-block card animate-in">${formatText(data)}</article>`;
50
+ }
51
+
52
+ // Object with text keys
53
+ if (data !== null && typeof data === 'object' && !Array.isArray(data)) {
54
+ const obj = data as Record<string, unknown>;
55
+ const sections: string[] = [];
56
+
57
+ for (const [key, value] of Object.entries(obj)) {
58
+ if (typeof value === 'string' && (value.length > 100 || (textKeys && textKeys.includes(key)))) {
59
+ const label = key.replace(/([A-Z])/g, ' $1').replace(/[_-]/g, ' ').replace(/^\w/, (c) => c.toUpperCase()).trim();
60
+ sections.push(`<section class="rb-section">
61
+ <h3 class="rb-section-title">${escapeHtml(label)}</h3>
62
+ <div class="rb-content">${formatText(value)}</div>
63
+ </section>`);
64
+ } else {
65
+ // Non-text fields → small metadata row
66
+ sections.push(`<div class="rb-meta-row">
67
+ <span class="rb-meta-key">${escapeHtml(key)}</span>
68
+ <span class="rb-meta-val">${escapeHtml(String(value))}</span>
69
+ </div>`);
70
+ }
71
+ }
72
+
73
+ return `<article class="reading-block card animate-in">${sections.join('\n')}</article>`;
74
+ }
75
+
76
+ // Fallback
77
+ return `<article class="reading-block card animate-in"><pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre></article>`;
78
+ }
79
+
80
+ export function getReadingBlockCSS(): string {
81
+ return `
82
+ .reading-block { max-width: 720px; line-height: 1.75; }
83
+ .reading-block p { margin-bottom: var(--sp-4); color: var(--text-primary); }
84
+ .reading-block strong { font-weight: 700; }
85
+
86
+ .rb-h1 { font-size: var(--text-2xl); font-weight: 800; margin: var(--sp-6) 0 var(--sp-3); }
87
+ .rb-h2 { font-size: var(--text-xl); font-weight: 700; margin: var(--sp-5) 0 var(--sp-2); }
88
+ .rb-h3 { font-size: var(--text-lg); font-weight: 600; margin: var(--sp-4) 0 var(--sp-2); }
89
+
90
+ .rb-code {
91
+ background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px;
92
+ font-family: var(--font-mono); font-size: 0.9em;
93
+ }
94
+ .rb-pre {
95
+ background: var(--bg-tertiary); padding: var(--sp-4); border-radius: var(--radius-sm);
96
+ overflow-x: auto; font-family: var(--font-mono); font-size: var(--text-sm);
97
+ margin: var(--sp-4) 0;
98
+ }
99
+
100
+ .rb-section { margin-bottom: var(--sp-6); }
101
+ .rb-section-title {
102
+ font-size: var(--text-base); font-weight: 700; color: var(--accent);
103
+ margin-bottom: var(--sp-2); padding-bottom: var(--sp-1); border-bottom: 1px solid var(--border);
104
+ }
105
+ .rb-content { color: var(--text-primary); }
106
+
107
+ .rb-meta-row {
108
+ display: flex; gap: var(--sp-3); padding: var(--sp-1) 0;
109
+ font-size: var(--text-sm); border-bottom: 1px solid var(--border);
110
+ }
111
+ .rb-meta-key { font-weight: 600; color: var(--text-secondary); min-width: 120px; }
112
+ .rb-meta-val { color: var(--text-primary); }
113
+ `;
114
+ }
package/src/theme.ts ADDED
@@ -0,0 +1,184 @@
1
+ // ── Theme system: CSS variables + dark mode + glassmorphism ──
2
+
3
+ export function generateThemeCSS(): string {
4
+ return `
5
+ /* ── Reset ── */
6
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
7
+
8
+ /* ── CSS Variables (Light) ── */
9
+ :root {
10
+ /* Surface */
11
+ --bg-primary: #ffffff;
12
+ --bg-secondary: #f8f9fb;
13
+ --bg-tertiary: #f0f2f5;
14
+ --bg-elevated: #ffffff;
15
+
16
+ /* Text */
17
+ --text-primary: #1a1d23;
18
+ --text-secondary: #5f6672;
19
+ --text-tertiary: #9aa0ab;
20
+ --text-inverse: #ffffff;
21
+
22
+ /* Accent */
23
+ --accent: #4f6ef7;
24
+ --accent-hover: #3b5ce4;
25
+ --accent-subtle: #eef1fe;
26
+ --accent-text: #ffffff;
27
+
28
+ /* Semantic */
29
+ --success: #22c55e;
30
+ --success-subtle: #f0fdf4;
31
+ --warning: #f59e0b;
32
+ --warning-subtle: #fffbeb;
33
+ --danger: #ef4444;
34
+ --danger-subtle: #fef2f2;
35
+ --info: #3b82f6;
36
+ --info-subtle: #eff6ff;
37
+
38
+ /* Borders */
39
+ --border: #e2e5ea;
40
+ --border-strong: #cdd1d8;
41
+
42
+ /* Shadows */
43
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
44
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
45
+ --shadow-lg: 0 12px 32px rgba(0,0,0,0.12);
46
+
47
+ /* Glass */
48
+ --glass-bg: rgba(255,255,255,0.72);
49
+ --glass-border: rgba(255,255,255,0.2);
50
+ --glass-blur: 16px;
51
+
52
+ /* Typography scale */
53
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
54
+ --font-mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
55
+ --text-xs: 0.75rem;
56
+ --text-sm: 0.875rem;
57
+ --text-base: 1rem;
58
+ --text-lg: 1.125rem;
59
+ --text-xl: 1.25rem;
60
+ --text-2xl: 1.5rem;
61
+ --text-3xl: 2rem;
62
+
63
+ /* Spacing scale */
64
+ --sp-1: 4px;
65
+ --sp-2: 8px;
66
+ --sp-3: 12px;
67
+ --sp-4: 16px;
68
+ --sp-5: 20px;
69
+ --sp-6: 24px;
70
+ --sp-8: 32px;
71
+ --sp-10: 40px;
72
+ --sp-12: 48px;
73
+
74
+ /* Radius */
75
+ --radius-sm: 6px;
76
+ --radius-md: 10px;
77
+ --radius-lg: 16px;
78
+ --radius-full: 9999px;
79
+
80
+ /* Transitions */
81
+ --ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
82
+ --duration-fast: 120ms;
83
+ --duration-normal: 200ms;
84
+ --duration-slow: 350ms;
85
+ }
86
+
87
+ /* ── Dark Mode ── */
88
+ @media (prefers-color-scheme: dark) {
89
+ :root {
90
+ --bg-primary: #0f1117;
91
+ --bg-secondary: #161922;
92
+ --bg-tertiary: #1e212b;
93
+ --bg-elevated: #1e212b;
94
+
95
+ --text-primary: #e8eaed;
96
+ --text-secondary: #9aa0ab;
97
+ --text-tertiary: #6b7280;
98
+ --text-inverse: #0f1117;
99
+
100
+ --accent: #6b8aff;
101
+ --accent-hover: #8ba3ff;
102
+ --accent-subtle: #1c2444;
103
+ --accent-text: #0f1117;
104
+
105
+ --success-subtle: #052e16;
106
+ --warning-subtle: #451a03;
107
+ --danger-subtle: #450a0a;
108
+ --info-subtle: #172554;
109
+
110
+ --border: #2a2d38;
111
+ --border-strong: #3b3f4c;
112
+
113
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
114
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
115
+ --shadow-lg: 0 12px 32px rgba(0,0,0,0.5);
116
+
117
+ --glass-bg: rgba(15,17,23,0.72);
118
+ --glass-border: rgba(255,255,255,0.08);
119
+ }
120
+ }
121
+
122
+ /* ── Base styles ── */
123
+ body {
124
+ font-family: var(--font-sans);
125
+ font-size: var(--text-base);
126
+ line-height: 1.6;
127
+ color: var(--text-primary);
128
+ background: var(--bg-primary);
129
+ padding: var(--sp-6);
130
+ -webkit-font-smoothing: antialiased;
131
+ }
132
+
133
+ /* ── Utility classes ── */
134
+ .glass {
135
+ background: var(--glass-bg);
136
+ backdrop-filter: blur(var(--glass-blur));
137
+ -webkit-backdrop-filter: blur(var(--glass-blur));
138
+ border: 1px solid var(--glass-border);
139
+ }
140
+
141
+ .card {
142
+ background: var(--bg-elevated);
143
+ border: 1px solid var(--border);
144
+ border-radius: var(--radius-md);
145
+ box-shadow: var(--shadow-sm);
146
+ padding: var(--sp-6);
147
+ }
148
+
149
+ .badge {
150
+ display: inline-flex;
151
+ align-items: center;
152
+ padding: 2px 8px;
153
+ border-radius: var(--radius-full);
154
+ font-size: var(--text-xs);
155
+ font-weight: 600;
156
+ text-transform: uppercase;
157
+ letter-spacing: 0.04em;
158
+ }
159
+
160
+ .badge-success { background: var(--success-subtle); color: var(--success); }
161
+ .badge-warning { background: var(--warning-subtle); color: var(--warning); }
162
+ .badge-danger { background: var(--danger-subtle); color: var(--danger); }
163
+ .badge-info { background: var(--info-subtle); color: var(--info); }
164
+
165
+ .section-title {
166
+ font-size: var(--text-lg);
167
+ font-weight: 700;
168
+ color: var(--text-primary);
169
+ margin-bottom: var(--sp-4);
170
+ padding-bottom: var(--sp-2);
171
+ border-bottom: 2px solid var(--border);
172
+ }
173
+
174
+ /* ── Animations ── */
175
+ @keyframes fadeIn {
176
+ from { opacity: 0; transform: translateY(8px); }
177
+ to { opacity: 1; transform: translateY(0); }
178
+ }
179
+
180
+ .animate-in {
181
+ animation: fadeIn var(--duration-normal) var(--ease-out) both;
182
+ }
183
+ `;
184
+ }
package/src/types.ts ADDED
@@ -0,0 +1,83 @@
1
+ // ── Render Intent ──
2
+ export type RenderIntent =
3
+ | 'form'
4
+ | 'data-grid'
5
+ | 'json-tree'
6
+ | 'reading-block'
7
+ | 'metrics-card'
8
+ | 'composite';
9
+
10
+ // ── Sniffer Result ──
11
+ export interface SniffResult {
12
+ intent: RenderIntent;
13
+ confidence: number;
14
+ metadata: Record<string, unknown>;
15
+ }
16
+
17
+ // ── Render Options ──
18
+ export interface RenderOptions {
19
+ darkMode?: boolean;
20
+ debug?: boolean;
21
+ title?: string;
22
+ }
23
+
24
+ // ── Engine Input (discriminated union) ──
25
+ export interface SchemaInput {
26
+ mode: 'schema';
27
+ schema: JSONSchema;
28
+ toolName?: string;
29
+ toolDescription?: string;
30
+ }
31
+
32
+ export interface DataInput {
33
+ mode: 'data';
34
+ data: unknown;
35
+ toolName?: string;
36
+ toolDescription?: string;
37
+ }
38
+
39
+ export type EngineInput = SchemaInput | DataInput;
40
+
41
+ // ── JSON Schema subset (sufficient for MCP tool schemas) ──
42
+ export interface JSONSchema {
43
+ type?: string | string[];
44
+ properties?: Record<string, JSONSchema>;
45
+ required?: string[];
46
+ items?: JSONSchema;
47
+ enum?: unknown[];
48
+ description?: string;
49
+ default?: unknown;
50
+ title?: string;
51
+ format?: string;
52
+ minimum?: number;
53
+ maximum?: number;
54
+ minLength?: number;
55
+ maxLength?: number;
56
+ pattern?: string;
57
+ oneOf?: JSONSchema[];
58
+ anyOf?: JSONSchema[];
59
+ allOf?: JSONSchema[];
60
+ $ref?: string;
61
+ additionalProperties?: boolean | JSONSchema;
62
+ }
63
+
64
+ // ── Renderer function signature ──
65
+ export type Renderer = (
66
+ data: unknown,
67
+ metadata: Record<string, unknown>,
68
+ themeCSS: string
69
+ ) => string;
70
+
71
+ // ── MCP Tool definition (from protocol) ──
72
+ export interface MCPToolDefinition {
73
+ name: string;
74
+ description?: string;
75
+ inputSchema: JSONSchema;
76
+ }
77
+
78
+ // ── MCP Client types ──
79
+ export interface MCPServerInfo {
80
+ name: string;
81
+ version: string;
82
+ tools: MCPToolDefinition[];
83
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }