@mcp-html-bridge/ui-engine 0.3.0 → 0.5.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.
@@ -1,159 +0,0 @@
1
- // ── Data Grid Renderer: Array of objects → sortable table ──
2
- import { escapeHtml } from '../html-builder.js';
3
-
4
- const STATUS_MAP: Record<string, string> = {
5
- active: 'badge-success',
6
- enabled: 'badge-success',
7
- available: 'badge-success',
8
- in_stock: 'badge-success',
9
- completed: 'badge-success',
10
- inactive: 'badge-danger',
11
- disabled: 'badge-danger',
12
- out_of_stock: 'badge-danger',
13
- backorder: 'badge-warning',
14
- pending: 'badge-warning',
15
- processing: 'badge-warning',
16
- low_stock: 'badge-warning',
17
- info: 'badge-info',
18
- draft: 'badge-info',
19
- };
20
-
21
- function formatCell(value: unknown, key: string): string {
22
- if (value === null || value === undefined) return '<span class="null">—</span>';
23
-
24
- const strVal = String(value);
25
-
26
- // Status badge detection
27
- const normalized = strVal.toLowerCase().replace(/[\s-]+/g, '_');
28
- if (STATUS_MAP[normalized]) {
29
- return `<span class="badge ${STATUS_MAP[normalized]}">${escapeHtml(strVal)}</span>`;
30
- }
31
-
32
- // Price formatting detection
33
- if (typeof value === 'number' && /price|cost|amount|total|revenue/i.test(key)) {
34
- return `<span class="price">$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>`;
35
- }
36
-
37
- // Number formatting
38
- if (typeof value === 'number') {
39
- return `<span class="num">${value.toLocaleString()}</span>`;
40
- }
41
-
42
- // Boolean
43
- if (typeof value === 'boolean') {
44
- return value ? '<span class="bool-true">✓</span>' : '<span class="bool-false">✗</span>';
45
- }
46
-
47
- // Nested object/array → compact JSON
48
- if (typeof value === 'object') {
49
- return `<code class="cell-json">${escapeHtml(JSON.stringify(value))}</code>`;
50
- }
51
-
52
- return escapeHtml(strVal);
53
- }
54
-
55
- function humanizeKey(key: string): string {
56
- return key
57
- .replace(/([A-Z])/g, ' $1')
58
- .replace(/[_-]/g, ' ')
59
- .replace(/^\w/, (c) => c.toUpperCase())
60
- .trim();
61
- }
62
-
63
- export function renderDataGrid(
64
- data: unknown,
65
- metadata: Record<string, unknown>
66
- ): string {
67
- const rows = data as Record<string, unknown>[];
68
- const columns = (metadata['columns'] as string[]) ?? Object.keys(rows[0] ?? {});
69
-
70
- const headerCells = columns
71
- .map(
72
- (col, i) =>
73
- `<th onclick="__mcpSort(${i})" class="sortable">${escapeHtml(humanizeKey(col))}<span class="sort-icon">⇅</span></th>`
74
- )
75
- .join('');
76
-
77
- const bodyRows = rows
78
- .map(
79
- (row, ri) =>
80
- `<tr class="animate-in" style="animation-delay:${Math.min(ri * 20, 300)}ms">${columns.map((col) => `<td>${formatCell(row[col], col)}</td>`).join('')}</tr>`
81
- )
82
- .join('\n');
83
-
84
- const rowCount = rows.length;
85
- const colCount = columns.length;
86
-
87
- return `<div class="grid-container card">
88
- <div class="grid-header">
89
- <span class="grid-meta">${rowCount} rows × ${colCount} columns</span>
90
- </div>
91
- <div class="table-wrap">
92
- <table class="data-table" id="mcp-grid">
93
- <thead><tr>${headerCells}</tr></thead>
94
- <tbody>${bodyRows}</tbody>
95
- </table>
96
- </div>
97
- </div>`;
98
- }
99
-
100
- export function getDataGridCSS(): string {
101
- return `
102
- .grid-container { overflow: hidden; }
103
- .grid-header {
104
- display: flex; justify-content: space-between; align-items: center;
105
- padding-bottom: var(--sp-3); margin-bottom: var(--sp-3); border-bottom: 1px solid var(--border);
106
- }
107
- .grid-meta { font-size: var(--text-xs); color: var(--text-tertiary); }
108
- .table-wrap { overflow-x: auto; }
109
- .data-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
110
- .data-table th {
111
- text-align: left; padding: var(--sp-2) var(--sp-3);
112
- font-weight: 600; color: var(--text-secondary); font-size: var(--text-xs);
113
- text-transform: uppercase; letter-spacing: 0.05em;
114
- border-bottom: 2px solid var(--border);
115
- white-space: nowrap; user-select: none;
116
- }
117
- .sortable { cursor: pointer; }
118
- .sortable:hover { color: var(--accent); }
119
- .sort-icon { margin-left: 4px; opacity: 0.3; font-size: 10px; }
120
- .data-table td {
121
- padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border);
122
- vertical-align: middle;
123
- }
124
- .data-table tbody tr:hover { background: var(--accent-subtle); }
125
- .data-table tbody tr:nth-child(even) { background: var(--bg-secondary); }
126
- .data-table tbody tr:nth-child(even):hover { background: var(--accent-subtle); }
127
-
128
- .null { color: var(--text-tertiary); }
129
- .price { font-weight: 600; font-variant-numeric: tabular-nums; }
130
- .num { font-variant-numeric: tabular-nums; }
131
- .bool-true { color: var(--success); font-weight: 700; }
132
- .bool-false { color: var(--danger); font-weight: 700; }
133
- .cell-json { font-size: var(--text-xs); max-width: 200px; overflow: hidden; text-overflow: ellipsis; display: inline-block; }
134
- `;
135
- }
136
-
137
- export function getDataGridJS(): string {
138
- return `
139
- function __mcpSort(colIdx) {
140
- var table = document.getElementById('mcp-grid');
141
- if (!table) return;
142
- var tbody = table.tBodies[0];
143
- var rows = Array.from(tbody.rows);
144
- var dir = table.dataset.sortDir === 'asc' ? 'desc' : 'asc';
145
- table.dataset.sortDir = dir;
146
- table.dataset.sortCol = colIdx;
147
-
148
- rows.sort(function(a, b) {
149
- var av = a.cells[colIdx].textContent.trim();
150
- var bv = b.cells[colIdx].textContent.trim();
151
- var an = parseFloat(av.replace(/[^\\d.-]/g, ''));
152
- var bn = parseFloat(bv.replace(/[^\\d.-]/g, ''));
153
- if (!isNaN(an) && !isNaN(bn)) return dir === 'asc' ? an - bn : bn - an;
154
- return dir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
155
- });
156
-
157
- rows.forEach(function(row) { tbody.appendChild(row); });
158
- }`;
159
- }
@@ -1,141 +0,0 @@
1
- // ── JSON Tree Renderer: recursive collapsible tree ──
2
- import { escapeHtml } from '../html-builder.js';
3
-
4
- function typeClass(val: unknown): string {
5
- if (val === null) return 'jt-null';
6
- if (typeof val === 'string') return 'jt-string';
7
- if (typeof val === 'number') return 'jt-number';
8
- if (typeof val === 'boolean') return 'jt-boolean';
9
- return '';
10
- }
11
-
12
- function formatValue(val: unknown): string {
13
- if (val === null) return '<span class="jt-null">null</span>';
14
- if (typeof val === 'string') return `<span class="jt-string">"${escapeHtml(val)}"</span>`;
15
- if (typeof val === 'number') return `<span class="jt-number">${val}</span>`;
16
- if (typeof val === 'boolean') return `<span class="jt-boolean">${val}</span>`;
17
- return escapeHtml(String(val));
18
- }
19
-
20
- function renderNode(key: string | null, val: unknown, depth: number): string {
21
- const keyHtml = key !== null ? `<span class="jt-key">${escapeHtml(key)}</span><span class="jt-colon">: </span>` : '';
22
- const indent = depth * 16;
23
- const nodeId = `jt-${Math.random().toString(36).slice(2, 8)}`;
24
-
25
- if (val === null || typeof val !== 'object') {
26
- return `<div class="jt-line" style="padding-left:${indent}px">
27
- ${keyHtml}${formatValue(val)}
28
- <button class="jt-copy" onclick="__mcpCopy(this)" data-val="${escapeHtml(JSON.stringify(val))}" title="Copy">⎘</button>
29
- </div>`;
30
- }
31
-
32
- const isArray = Array.isArray(val);
33
- const entries = isArray
34
- ? (val as unknown[]).map((v, i) => [String(i), v] as const)
35
- : Object.entries(val as Record<string, unknown>);
36
-
37
- const bracket = isArray ? ['[', ']'] : ['{', '}'];
38
- const count = entries.length;
39
- const preview = count === 0 ? (isArray ? '[]' : '{}') : '';
40
-
41
- if (count === 0) {
42
- return `<div class="jt-line" style="padding-left:${indent}px">${keyHtml}<span class="jt-bracket">${preview}</span></div>`;
43
- }
44
-
45
- const children = entries.map(([k, v]) => renderNode(isArray ? null : k, v, depth + 1)).join('\n');
46
-
47
- return `<div class="jt-node" style="padding-left:${indent}px">
48
- <div class="jt-toggle" onclick="__mcpToggle('${nodeId}')">
49
- <span class="jt-arrow" id="${nodeId}-arrow">▼</span>
50
- ${keyHtml}<span class="jt-bracket">${bracket[0]}</span>
51
- <span class="jt-count">${count} ${isArray ? 'items' : 'keys'}</span>
52
- <button class="jt-copy" onclick="event.stopPropagation();__mcpCopy(this)" data-val="${escapeHtml(JSON.stringify(val))}" title="Copy">⎘</button>
53
- </div>
54
- <div class="jt-children" id="${nodeId}">${children}</div>
55
- <div style="padding-left:0"><span class="jt-bracket">${bracket[1]}</span></div>
56
- </div>`;
57
- }
58
-
59
- export function renderJsonTree(
60
- data: unknown,
61
- _metadata: Record<string, unknown>
62
- ): string {
63
- return `<div class="json-tree card">
64
- <div class="jt-toolbar">
65
- <button class="btn btn-ghost btn-sm" onclick="__mcpExpandAll()">Expand All</button>
66
- <button class="btn btn-ghost btn-sm" onclick="__mcpCollapseAll()">Collapse All</button>
67
- <button class="btn btn-ghost btn-sm" onclick="__mcpCopyAll()">Copy JSON</button>
68
- </div>
69
- <div class="jt-root">${renderNode(null, data, 0)}</div>
70
- </div>`;
71
- }
72
-
73
- export function getJsonTreeCSS(): string {
74
- return `
75
- .json-tree { font-family: var(--font-mono); font-size: var(--text-sm); overflow-x: auto; }
76
- .jt-toolbar { display: flex; gap: var(--sp-2); margin-bottom: var(--sp-3); padding-bottom: var(--sp-3); border-bottom: 1px solid var(--border); }
77
- .btn-sm { padding: var(--sp-1) var(--sp-2); font-size: var(--text-xs); }
78
-
79
- .jt-line, .jt-node > .jt-toggle { padding: 2px 0; display: flex; align-items: center; gap: 4px; }
80
- .jt-toggle { cursor: pointer; user-select: none; }
81
- .jt-toggle:hover { background: var(--accent-subtle); border-radius: var(--radius-sm); }
82
- .jt-arrow { font-size: 10px; width: 14px; text-align: center; transition: transform var(--duration-fast) var(--ease-out); display: inline-block; }
83
- .jt-arrow.collapsed { transform: rotate(-90deg); }
84
-
85
- .jt-key { color: var(--accent); font-weight: 600; }
86
- .jt-colon { color: var(--text-tertiary); }
87
- .jt-string { color: #22863a; }
88
- .jt-number { color: #005cc5; }
89
- .jt-boolean { color: #d73a49; }
90
- .jt-null { color: var(--text-tertiary); font-style: italic; }
91
- .jt-bracket { color: var(--text-tertiary); font-weight: 700; }
92
- .jt-count { color: var(--text-tertiary); font-size: var(--text-xs); margin-left: var(--sp-1); }
93
-
94
- @media (prefers-color-scheme: dark) {
95
- .jt-string { color: #7ee787; }
96
- .jt-number { color: #79c0ff; }
97
- .jt-boolean { color: #ff7b72; }
98
- }
99
-
100
- .jt-children { overflow: hidden; }
101
- .jt-children.hidden { display: none; }
102
- .jt-copy {
103
- opacity: 0; border: none; background: none; cursor: pointer;
104
- color: var(--text-tertiary); font-size: 14px; padding: 0 4px;
105
- transition: opacity var(--duration-fast) var(--ease-out);
106
- }
107
- .jt-line:hover .jt-copy, .jt-toggle:hover .jt-copy { opacity: 1; }
108
- .jt-copy:hover { color: var(--accent); }
109
- `;
110
- }
111
-
112
- export function getJsonTreeJS(): string {
113
- return `
114
- var __mcpTreeData;
115
- function __mcpToggle(id) {
116
- var el = document.getElementById(id);
117
- var arrow = document.getElementById(id + '-arrow');
118
- if (!el) return;
119
- el.classList.toggle('hidden');
120
- if (arrow) arrow.classList.toggle('collapsed');
121
- }
122
- function __mcpExpandAll() {
123
- document.querySelectorAll('.jt-children').forEach(function(el) { el.classList.remove('hidden'); });
124
- document.querySelectorAll('.jt-arrow').forEach(function(el) { el.classList.remove('collapsed'); });
125
- }
126
- function __mcpCollapseAll() {
127
- document.querySelectorAll('.jt-children').forEach(function(el) { el.classList.add('hidden'); });
128
- document.querySelectorAll('.jt-arrow').forEach(function(el) { el.classList.add('collapsed'); });
129
- }
130
- function __mcpCopy(btn) {
131
- var val = btn.getAttribute('data-val');
132
- navigator.clipboard.writeText(val).then(function() {
133
- var orig = btn.textContent;
134
- btn.textContent = '✓';
135
- setTimeout(function() { btn.textContent = orig; }, 1000);
136
- });
137
- }
138
- function __mcpCopyAll() {
139
- if (__mcpTreeData) navigator.clipboard.writeText(JSON.stringify(__mcpTreeData, null, 2));
140
- }`;
141
- }
@@ -1,108 +0,0 @@
1
- // ── Metrics Card Renderer: KPI/summary → card layout ──
2
- import { escapeHtml } from '../html-builder.js';
3
-
4
- function formatMetricValue(value: number, key: string): string {
5
- // Percentage
6
- if (/rate|ratio|percent|pct/i.test(key)) {
7
- return `${(value * (value <= 1 ? 100 : 1)).toFixed(1)}%`;
8
- }
9
- // Currency
10
- if (/price|cost|revenue|amount|total|value/i.test(key)) {
11
- if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`;
12
- if (value >= 1_000) return `$${(value / 1_000).toFixed(1)}K`;
13
- return `$${value.toFixed(2)}`;
14
- }
15
- // Large numbers
16
- if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
17
- if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
18
- // Scientific notation for very small/large
19
- if (Math.abs(value) < 0.001 && value !== 0) return value.toExponential(2);
20
-
21
- return Number.isInteger(value) ? value.toLocaleString() : value.toFixed(2);
22
- }
23
-
24
- function humanizeKey(key: string): string {
25
- return key
26
- .replace(/([A-Z])/g, ' $1')
27
- .replace(/[_-]/g, ' ')
28
- .replace(/^\w/, (c) => c.toUpperCase())
29
- .trim();
30
- }
31
-
32
- export function renderMetricsCard(
33
- data: unknown,
34
- _metadata: Record<string, unknown>
35
- ): string {
36
- if (data === null || typeof data !== 'object' || Array.isArray(data)) {
37
- return '<div class="card">Invalid metrics data</div>';
38
- }
39
-
40
- const obj = data as Record<string, unknown>;
41
- const cards: string[] = [];
42
-
43
- for (const [key, value] of Object.entries(obj)) {
44
- if (typeof value === 'number') {
45
- const formatted = formatMetricValue(value, key);
46
- const label = humanizeKey(key);
47
- cards.push(`<div class="metric-card animate-in">
48
- <div class="metric-label">${escapeHtml(label)}</div>
49
- <div class="metric-value">${escapeHtml(formatted)}</div>
50
- </div>`);
51
- } else if (typeof value === 'string') {
52
- cards.push(`<div class="metric-card metric-text animate-in">
53
- <div class="metric-label">${escapeHtml(humanizeKey(key))}</div>
54
- <div class="metric-text-value">${escapeHtml(value)}</div>
55
- </div>`);
56
- }
57
- }
58
-
59
- return `<div class="metrics-grid">${cards.join('\n')}</div>`;
60
- }
61
-
62
- export function getMetricsCardCSS(): string {
63
- return `
64
- .metrics-grid {
65
- display: grid;
66
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
67
- gap: var(--sp-4);
68
- }
69
-
70
- .metric-card {
71
- background: var(--bg-elevated);
72
- border: 1px solid var(--border);
73
- border-radius: var(--radius-md);
74
- padding: var(--sp-5);
75
- display: flex;
76
- flex-direction: column;
77
- gap: var(--sp-2);
78
- transition: box-shadow var(--duration-fast) var(--ease-out),
79
- transform var(--duration-fast) var(--ease-out);
80
- }
81
- .metric-card:hover {
82
- box-shadow: var(--shadow-md);
83
- transform: translateY(-2px);
84
- }
85
-
86
- .metric-label {
87
- font-size: var(--text-xs);
88
- font-weight: 600;
89
- text-transform: uppercase;
90
- letter-spacing: 0.05em;
91
- color: var(--text-tertiary);
92
- }
93
-
94
- .metric-value {
95
- font-size: var(--text-3xl);
96
- font-weight: 800;
97
- color: var(--text-primary);
98
- font-variant-numeric: tabular-nums;
99
- letter-spacing: -0.02em;
100
- }
101
-
102
- .metric-text-value {
103
- font-size: var(--text-base);
104
- font-weight: 500;
105
- color: var(--text-secondary);
106
- }
107
- `;
108
- }
@@ -1,114 +0,0 @@
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
- }