@mcp-html-bridge/ui-engine 0.4.0 → 0.5.1
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.
- package/package.json +1 -1
- package/src/engine.ts +43 -10
- package/src/index.ts +7 -1
- package/src/llm-renderer.ts +129 -0
- package/src/renderer.ts +143 -29
- package/src/utilities.ts +319 -0
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
// ── Engine Orchestrator: JSON → self-contained HTML document ──
|
|
2
2
|
import type { EngineInput, RenderOptions, JSONSchema } from './types.js';
|
|
3
|
+
import type { LLMConfig } from './llm-renderer.js';
|
|
3
4
|
import { document as htmlDocument } from './html-builder.js';
|
|
4
5
|
import { generateThemeCSS } from './theme.js';
|
|
5
6
|
import { generateBridgeJS } from './bridge.js';
|
|
6
7
|
import { renderJSON, getRendererCSS, getRendererJS } from './renderer.js';
|
|
8
|
+
import { renderWithLLM } from './llm-renderer.js';
|
|
7
9
|
import { renderForm, getFormCSS } from './renderers/form.js';
|
|
8
10
|
import { generatePlaygroundHTML, getPlaygroundCSS, getPlaygroundJS } from './playground.js';
|
|
11
|
+
import { generateUtilityCSS } from './utilities.js';
|
|
9
12
|
|
|
10
13
|
/** Options for rendering */
|
|
11
14
|
interface DataRenderOptions extends RenderOptions {
|
|
12
15
|
toolName?: string;
|
|
13
16
|
toolDescription?: string;
|
|
17
|
+
/** LLM config for semantic rendering. Omit for structural fallback. */
|
|
18
|
+
llm?: LLMConfig;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
): string {
|
|
21
|
-
const body = renderJSON(data);
|
|
22
|
-
const cssParts = [generateThemeCSS(), getRendererCSS()];
|
|
23
|
-
const jsParts = [generateBridgeJS(), getRendererJS()];
|
|
21
|
+
/** Build the document wrapper around an HTML body fragment */
|
|
22
|
+
function buildDocument(body: string, options: DataRenderOptions, extraCSS = '', extraJS = ''): string {
|
|
23
|
+
const cssParts = [generateThemeCSS(), generateUtilityCSS(), extraCSS];
|
|
24
|
+
const jsParts = [generateBridgeJS(), extraJS];
|
|
24
25
|
|
|
25
26
|
if (options.debug) {
|
|
26
27
|
cssParts.push(getPlaygroundCSS());
|
|
@@ -37,7 +38,38 @@ export function renderFromData(
|
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Render any JSON data as a full HTML document.
|
|
43
|
+
*
|
|
44
|
+
* - If `options.llm` is provided: calls the LLM for semantic rendering (async).
|
|
45
|
+
* - If omitted: uses the structural renderer (sync, returned as resolved Promise).
|
|
46
|
+
*/
|
|
47
|
+
export async function renderFromData(
|
|
48
|
+
data: unknown,
|
|
49
|
+
options: DataRenderOptions = {}
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
if (options.llm) {
|
|
52
|
+
const body = await renderWithLLM(data, options.llm);
|
|
53
|
+
// Always include structural CSS/JS — the LLM fallback path may produce structural HTML
|
|
54
|
+
return buildDocument(body, options, getRendererCSS(), getRendererJS());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const body = renderJSON(data);
|
|
58
|
+
return buildDocument(body, options, getRendererCSS(), getRendererJS());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sync structural rendering (no LLM). Use when you know you don't need LLM.
|
|
63
|
+
*/
|
|
64
|
+
export function renderFromDataSync(
|
|
65
|
+
data: unknown,
|
|
66
|
+
options: Omit<DataRenderOptions, 'llm'> = {}
|
|
67
|
+
): string {
|
|
68
|
+
const body = renderJSON(data);
|
|
69
|
+
return buildDocument(body, options, getRendererCSS(), getRendererJS());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Render a form from a JSON Schema (for tool input). Always sync. */
|
|
41
73
|
export function renderFromSchema(
|
|
42
74
|
schema: JSONSchema,
|
|
43
75
|
options: DataRenderOptions = {}
|
|
@@ -51,6 +83,7 @@ export function renderFromSchema(
|
|
|
51
83
|
|
|
52
84
|
const css = [
|
|
53
85
|
generateThemeCSS(),
|
|
86
|
+
generateUtilityCSS(),
|
|
54
87
|
getFormCSS(),
|
|
55
88
|
options.debug ? getPlaygroundCSS() : '',
|
|
56
89
|
].join('\n');
|
|
@@ -69,7 +102,7 @@ export function renderFromSchema(
|
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
/** Unified API */
|
|
72
|
-
export function render(input: EngineInput, options: DataRenderOptions = {}): string {
|
|
105
|
+
export async function render(input: EngineInput, options: DataRenderOptions = {}): Promise<string> {
|
|
73
106
|
if (input.mode === 'schema') {
|
|
74
107
|
return renderFromSchema(input.schema, {
|
|
75
108
|
...options,
|
package/src/index.ts
CHANGED
|
@@ -3,13 +3,19 @@ export {
|
|
|
3
3
|
render,
|
|
4
4
|
renderFromSchema,
|
|
5
5
|
renderFromData,
|
|
6
|
+
renderFromDataSync,
|
|
6
7
|
} from './engine.js';
|
|
7
8
|
|
|
8
|
-
// Universal JSON → HTML renderer (
|
|
9
|
+
// Universal JSON → HTML renderer (structural fallback)
|
|
9
10
|
export { renderJSON, getRendererCSS, getRendererJS } from './renderer.js';
|
|
10
11
|
|
|
12
|
+
// LLM-powered semantic renderer
|
|
13
|
+
export { renderWithLLM, RENDERING_PROMPT } from './llm-renderer.js';
|
|
14
|
+
export type { LLMConfig } from './llm-renderer.js';
|
|
15
|
+
|
|
11
16
|
// Building blocks (for custom composition)
|
|
12
17
|
export { generateThemeCSS } from './theme.js';
|
|
18
|
+
export { generateUtilityCSS } from './utilities.js';
|
|
13
19
|
export { generateBridgeJS } from './bridge.js';
|
|
14
20
|
export { escapeHtml, tag, style, script, document } from './html-builder.js';
|
|
15
21
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-powered semantic renderer.
|
|
3
|
+
*
|
|
4
|
+
* The structural renderer (renderer.ts) maps JSON shapes to HTML mechanically.
|
|
5
|
+
* This module sends JSON to an LLM with a rendering prompt,
|
|
6
|
+
* letting the model understand semantics and produce the best HTML.
|
|
7
|
+
*
|
|
8
|
+
* Supports any OpenAI-compatible API.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Configuration for the LLM endpoint */
|
|
12
|
+
export interface LLMConfig {
|
|
13
|
+
/** API base URL (e.g. "http://localhost:11434/v1") */
|
|
14
|
+
apiUrl: string;
|
|
15
|
+
/** API key (optional for local models) */
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
/** Model name */
|
|
18
|
+
model: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The rendering prompt.
|
|
23
|
+
*
|
|
24
|
+
* This is intentionally short and non-prescriptive. We tell the model
|
|
25
|
+
* WHAT to do (produce the best HTML for this data), not HOW to do it
|
|
26
|
+
* (don't enumerate SVG, markdown, etc.). The model should figure out
|
|
27
|
+
* the semantics on its own.
|
|
28
|
+
*/
|
|
29
|
+
const SYSTEM_PROMPT = `You are a JSON-to-HTML renderer. You receive JSON data and produce an HTML fragment that visualizes it in the best way possible.
|
|
30
|
+
|
|
31
|
+
Rules:
|
|
32
|
+
1. Output ONLY an HTML fragment. No <html>, <head>, <body> wrappers — those exist already.
|
|
33
|
+
2. Understand what the data MEANS, not just its shape. Render content in its native form — if something is meant to be seen, show it; if it's meant to be read, format it; if it's structured, organize it.
|
|
34
|
+
3. Use these CSS variables for theming (light/dark mode is handled automatically):
|
|
35
|
+
Colors: --bg-primary, --bg-secondary, --bg-tertiary, --bg-elevated, --text-primary, --text-secondary, --text-tertiary, --accent, --accent-subtle, --success, --warning, --danger, --info (each has a -subtle variant), --border, --border-strong
|
|
36
|
+
Typography: --font-sans, --font-mono, --text-xs to --text-3xl
|
|
37
|
+
Spacing: --sp-1 (4px) to --sp-12 (48px)
|
|
38
|
+
Radius: --radius-sm, --radius-md, --radius-lg, --radius-full
|
|
39
|
+
Shadows: --shadow-sm, --shadow-md, --shadow-lg
|
|
40
|
+
Utility classes: .card, .badge, .badge-success, .badge-warning, .badge-danger, .badge-info, .section-title
|
|
41
|
+
4. Tailwind-compatible utility classes are available. Use them freely: flex, grid, gap-*, p-*, m-*, text-*, bg-*, border, rounded-*, shadow-*, font-*, items-*, justify-*, w-full, h-full, overflow-*, relative/absolute, etc. Responsive prefixes sm: and md: work too.
|
|
42
|
+
5. Do not invent data. Do not add commentary. Just the HTML.
|
|
43
|
+
6. Do not wrap output in markdown code fences.`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Call an OpenAI-compatible chat completions API.
|
|
47
|
+
*/
|
|
48
|
+
async function callLLM(
|
|
49
|
+
data: unknown,
|
|
50
|
+
config: LLMConfig
|
|
51
|
+
): Promise<string> {
|
|
52
|
+
const url = config.apiUrl.replace(/\/+$/, '') + '/chat/completions';
|
|
53
|
+
|
|
54
|
+
const headers: Record<string, string> = {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
};
|
|
57
|
+
if (config.apiKey) {
|
|
58
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const body = {
|
|
62
|
+
model: config.model,
|
|
63
|
+
messages: [
|
|
64
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
65
|
+
{ role: 'user', content: JSON.stringify(data, null, 2) },
|
|
66
|
+
],
|
|
67
|
+
temperature: 0.2,
|
|
68
|
+
max_tokens: 16384,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers,
|
|
74
|
+
body: JSON.stringify(body),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const text = await response.text();
|
|
79
|
+
throw new Error(`LLM API ${response.status}: ${text.slice(0, 300)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await response.json() as {
|
|
83
|
+
choices?: Array<{ message?: { content?: string } }>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const content = result.choices?.[0]?.message?.content;
|
|
87
|
+
if (!content) {
|
|
88
|
+
throw new Error('LLM returned empty response');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return stripCodeFences(content);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Remove markdown code fences if the LLM wrapped its output */
|
|
95
|
+
function stripCodeFences(html: string): string {
|
|
96
|
+
const trimmed = html.trim();
|
|
97
|
+
const fenceMatch = trimmed.match(/^```(?:html)?\s*\n([\s\S]*?)\n\s*```$/);
|
|
98
|
+
if (fenceMatch) {
|
|
99
|
+
return fenceMatch[1].trim();
|
|
100
|
+
}
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Render JSON data using an LLM for semantic understanding.
|
|
106
|
+
* Returns an HTML fragment (not a full document).
|
|
107
|
+
* Falls back to structural renderer on failure.
|
|
108
|
+
*/
|
|
109
|
+
export async function renderWithLLM(
|
|
110
|
+
data: unknown,
|
|
111
|
+
config: LLMConfig
|
|
112
|
+
): Promise<string> {
|
|
113
|
+
try {
|
|
114
|
+
const html = await callLLM(data, config);
|
|
115
|
+
return `<div class="mcp-root">${html}</div>`;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
118
|
+
console.error(` LLM rendering failed: ${message}`);
|
|
119
|
+
console.error(' Falling back to structural renderer.');
|
|
120
|
+
|
|
121
|
+
const { renderJSON } = await import('./renderer.js');
|
|
122
|
+
const fallbackHTML = renderJSON(data);
|
|
123
|
+
const errorBanner = `<div style="padding:var(--sp-3);margin-bottom:var(--sp-4);background:var(--warning-subtle);border:1px solid var(--warning);border-radius:var(--radius-sm);font-size:var(--text-sm);color:var(--text-primary)">LLM rendering failed: ${message.replace(/</g, '<')}. Showing structural fallback.</div>`;
|
|
124
|
+
return `<div class="mcp-root">${errorBanner}${fallbackHTML}</div>`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Export the system prompt for inspection/customization */
|
|
129
|
+
export { SYSTEM_PROMPT as RENDERING_PROMPT };
|
package/src/renderer.ts
CHANGED
|
@@ -2,32 +2,49 @@
|
|
|
2
2
|
* Universal JSON → HTML renderer.
|
|
3
3
|
*
|
|
4
4
|
* No hardcoded business logic. No status badges, no price formatting,
|
|
5
|
-
* no regex-based field type guessing. Just clean structural rendering
|
|
5
|
+
* no regex-based field type guessing. Just clean structural rendering
|
|
6
|
+
* that handles any JSON shape gracefully:
|
|
6
7
|
*
|
|
7
|
-
* Array<Object> → sortable <table>
|
|
8
|
+
* Array<Object> → sortable <table> (union of all keys)
|
|
8
9
|
* Object → <dl> key-value pairs (recursive for nesting)
|
|
9
|
-
* Array<
|
|
10
|
-
* string → text
|
|
10
|
+
* Array<mixed> → <ul> list with recursive items
|
|
11
|
+
* string → text (auto-links URLs)
|
|
11
12
|
* number → number
|
|
12
13
|
* boolean → true/false
|
|
13
14
|
* null → placeholder
|
|
15
|
+
* empty → explicit empty indicator
|
|
14
16
|
*
|
|
15
17
|
* All formatting decisions are the caller's responsibility (LLM or user).
|
|
16
18
|
*/
|
|
17
19
|
import { escapeHtml } from './html-builder.js';
|
|
18
20
|
|
|
21
|
+
// ── Constants ──
|
|
22
|
+
|
|
23
|
+
const MAX_DEPTH = 30;
|
|
24
|
+
const MAX_TABLE_ROWS = 500;
|
|
25
|
+
const MAX_LIST_ITEMS = 200;
|
|
26
|
+
const URL_REGEX = /^https?:\/\/[^\s<>"{}|\\^`[\]]+$/;
|
|
27
|
+
const IMAGE_EXT_REGEX = /\.(png|jpe?g|gif|webp|svg|ico|bmp)(\?[^\s]*)?$/i;
|
|
28
|
+
|
|
19
29
|
// ── Structural detection ──
|
|
20
30
|
|
|
21
31
|
function isArrayOfObjects(data: unknown): data is Record<string, unknown>[] {
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
if (!Array.isArray(data) || data.length === 0) return false;
|
|
33
|
+
// At least 80% must be non-null objects (tolerates a few nulls in the array)
|
|
34
|
+
let objCount = 0;
|
|
35
|
+
for (const item of data) {
|
|
36
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
|
37
|
+
objCount++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return objCount / data.length >= 0.8 && objCount >= 1;
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
function isFlatObject(data: unknown): data is Record<string, unknown> {
|
|
27
44
|
if (data === null || typeof data !== 'object' || Array.isArray(data)) return false;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
45
|
+
const values = Object.values(data as Record<string, unknown>);
|
|
46
|
+
if (values.length === 0) return true;
|
|
47
|
+
return values.every(v => v === null || typeof v !== 'object');
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
function humanizeKey(key: string): string {
|
|
@@ -38,19 +55,38 @@ function humanizeKey(key: string): string {
|
|
|
38
55
|
.trim();
|
|
39
56
|
}
|
|
40
57
|
|
|
58
|
+
function isUrl(value: string): boolean {
|
|
59
|
+
return URL_REGEX.test(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isImageUrl(value: string): boolean {
|
|
63
|
+
return IMAGE_EXT_REGEX.test(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
// ── Render primitives ──
|
|
42
67
|
|
|
43
68
|
function renderValue(value: unknown, depth: number): string {
|
|
44
69
|
if (value === null || value === undefined) {
|
|
45
|
-
return '<span class="mcp-null"
|
|
70
|
+
return '<span class="mcp-null">\u2014</span>';
|
|
46
71
|
}
|
|
47
72
|
if (typeof value === 'boolean') {
|
|
48
|
-
return `<span class="mcp-bool">${value}</span>`;
|
|
73
|
+
return `<span class="mcp-bool mcp-bool-${value}">${value}</span>`;
|
|
49
74
|
}
|
|
50
75
|
if (typeof value === 'number') {
|
|
51
76
|
return `<span class="mcp-num">${value}</span>`;
|
|
52
77
|
}
|
|
53
78
|
if (typeof value === 'string') {
|
|
79
|
+
if (value.length === 0) {
|
|
80
|
+
return '<span class="mcp-null">(empty)</span>';
|
|
81
|
+
}
|
|
82
|
+
// Auto-link URLs
|
|
83
|
+
if (isUrl(value)) {
|
|
84
|
+
const escaped = escapeHtml(value);
|
|
85
|
+
if (isImageUrl(value)) {
|
|
86
|
+
return `<a href="${escaped}" target="_blank" rel="noopener"><img class="mcp-img" src="${escaped}" alt="" loading="lazy"></a>`;
|
|
87
|
+
}
|
|
88
|
+
return `<a class="mcp-link" href="${escaped}" target="_blank" rel="noopener">${escaped}</a>`;
|
|
89
|
+
}
|
|
54
90
|
if (value.length > 300) {
|
|
55
91
|
return `<div class="mcp-text">${escapeHtml(value)}</div>`;
|
|
56
92
|
}
|
|
@@ -62,22 +98,49 @@ function renderValue(value: unknown, depth: number): string {
|
|
|
62
98
|
|
|
63
99
|
// ── Core recursive renderer ──
|
|
64
100
|
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
/** Collect union of all keys across an array of objects, preserving order */
|
|
102
|
+
function collectColumns(rows: Record<string, unknown>[]): string[] {
|
|
103
|
+
const seen = new Set<string>();
|
|
104
|
+
const columns: string[] = [];
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
for (const key of Object.keys(row)) {
|
|
107
|
+
if (!seen.has(key)) {
|
|
108
|
+
seen.add(key);
|
|
109
|
+
columns.push(key);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return columns;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderTable(rows: Record<string, unknown>[], tableId: string): string {
|
|
117
|
+
const columns = collectColumns(rows);
|
|
118
|
+
const truncated = rows.length > MAX_TABLE_ROWS;
|
|
119
|
+
const displayRows = truncated ? rows.slice(0, MAX_TABLE_ROWS) : rows;
|
|
120
|
+
|
|
67
121
|
const headerCells = columns
|
|
68
122
|
.map((col, i) =>
|
|
69
|
-
`<th onclick="__mcpSort(${i})" class="mcp-sortable">${escapeHtml(humanizeKey(col))}<span class="mcp-sort-icon"
|
|
123
|
+
`<th onclick="__mcpSort('${escapeHtml(tableId)}',${i})" class="mcp-sortable">${escapeHtml(humanizeKey(col))}<span class="mcp-sort-icon">\u21C5</span></th>`)
|
|
70
124
|
.join('');
|
|
71
125
|
|
|
72
|
-
const bodyRows =
|
|
73
|
-
.map(row =>
|
|
74
|
-
|
|
126
|
+
const bodyRows = displayRows
|
|
127
|
+
.map(row => {
|
|
128
|
+
const cells = columns.map(col => {
|
|
129
|
+
const val = Object.prototype.hasOwnProperty.call(row, col) ? row[col] : undefined;
|
|
130
|
+
return `<td>${renderValue(val, 1)}</td>`;
|
|
131
|
+
}).join('');
|
|
132
|
+
return `<tr>${cells}</tr>`;
|
|
133
|
+
})
|
|
75
134
|
.join('\n');
|
|
76
135
|
|
|
136
|
+
const meta = truncated
|
|
137
|
+
? `${rows.length} rows \u00D7 ${columns.length} columns (showing first ${MAX_TABLE_ROWS})`
|
|
138
|
+
: `${rows.length} rows \u00D7 ${columns.length} columns`;
|
|
139
|
+
|
|
77
140
|
return `<div class="mcp-table-wrap">
|
|
78
|
-
<div class="mcp-table-meta">${
|
|
141
|
+
<div class="mcp-table-meta">${meta}</div>
|
|
79
142
|
<div class="mcp-table-scroll">
|
|
80
|
-
<table class="mcp-table" id="
|
|
143
|
+
<table class="mcp-table" id="${escapeHtml(tableId)}">
|
|
81
144
|
<thead><tr>${headerCells}</tr></thead>
|
|
82
145
|
<tbody>${bodyRows}</tbody>
|
|
83
146
|
</table>
|
|
@@ -87,6 +150,9 @@ function renderTable(rows: Record<string, unknown>[]): string {
|
|
|
87
150
|
|
|
88
151
|
function renderKeyValue(obj: Record<string, unknown>, depth: number): string {
|
|
89
152
|
const entries = Object.entries(obj);
|
|
153
|
+
if (entries.length === 0) {
|
|
154
|
+
return '<div class="mcp-empty">(empty object)</div>';
|
|
155
|
+
}
|
|
90
156
|
const items = entries.map(([key, val]) => {
|
|
91
157
|
const rendered = renderValue(val, depth);
|
|
92
158
|
return `<div class="mcp-kv">
|
|
@@ -99,10 +165,21 @@ function renderKeyValue(obj: Record<string, unknown>, depth: number): string {
|
|
|
99
165
|
}
|
|
100
166
|
|
|
101
167
|
function renderList(arr: unknown[], depth: number): string {
|
|
102
|
-
|
|
168
|
+
if (arr.length === 0) {
|
|
169
|
+
return '<div class="mcp-empty">(empty array)</div>';
|
|
170
|
+
}
|
|
171
|
+
const truncated = arr.length > MAX_LIST_ITEMS;
|
|
172
|
+
const displayItems = truncated ? arr.slice(0, MAX_LIST_ITEMS) : arr;
|
|
173
|
+
|
|
174
|
+
const items = displayItems.map(item =>
|
|
103
175
|
`<li>${renderValue(item, depth)}</li>`
|
|
104
176
|
).join('\n');
|
|
105
|
-
|
|
177
|
+
|
|
178
|
+
const suffix = truncated
|
|
179
|
+
? `<li class="mcp-truncated">\u2026 and ${arr.length - MAX_LIST_ITEMS} more items</li>`
|
|
180
|
+
: '';
|
|
181
|
+
|
|
182
|
+
return `<ul class="mcp-list">${items}\n${suffix}</ul>`;
|
|
106
183
|
}
|
|
107
184
|
|
|
108
185
|
function renderCollapsible(label: string, content: string, open = true): string {
|
|
@@ -112,7 +189,15 @@ function renderCollapsible(label: string, content: string, open = true): string
|
|
|
112
189
|
</details>`;
|
|
113
190
|
}
|
|
114
191
|
|
|
192
|
+
// Global table counter for unique IDs
|
|
193
|
+
let tableCounter = 0;
|
|
194
|
+
|
|
115
195
|
function renderAny(data: unknown, depth: number): string {
|
|
196
|
+
// Depth guard
|
|
197
|
+
if (depth > MAX_DEPTH) {
|
|
198
|
+
return '<span class="mcp-null">(max depth reached)</span>';
|
|
199
|
+
}
|
|
200
|
+
|
|
116
201
|
// Primitive
|
|
117
202
|
if (data === null || data === undefined || typeof data !== 'object') {
|
|
118
203
|
return renderValue(data, depth);
|
|
@@ -120,7 +205,13 @@ function renderAny(data: unknown, depth: number): string {
|
|
|
120
205
|
|
|
121
206
|
// Array of objects → table
|
|
122
207
|
if (isArrayOfObjects(data)) {
|
|
123
|
-
|
|
208
|
+
// Filter to only objects for the table, skip non-objects
|
|
209
|
+
const objectRows = (data as unknown[]).filter(
|
|
210
|
+
(item): item is Record<string, unknown> =>
|
|
211
|
+
item !== null && typeof item === 'object' && !Array.isArray(item)
|
|
212
|
+
);
|
|
213
|
+
const tableId = `mcp-grid-${tableCounter++}`;
|
|
214
|
+
const content = renderTable(objectRows, tableId);
|
|
124
215
|
return depth > 0 ? renderCollapsible(`Array (${data.length} items)`, content) : content;
|
|
125
216
|
}
|
|
126
217
|
|
|
@@ -138,6 +229,10 @@ function renderAny(data: unknown, depth: number): string {
|
|
|
138
229
|
|
|
139
230
|
// Nested object → grouped sections
|
|
140
231
|
const entries = Object.entries(obj);
|
|
232
|
+
if (entries.length === 0) {
|
|
233
|
+
return '<div class="mcp-empty">(empty object)</div>';
|
|
234
|
+
}
|
|
235
|
+
|
|
141
236
|
const sections = entries.map(([key, val]) => {
|
|
142
237
|
if (val !== null && typeof val === 'object') {
|
|
143
238
|
return renderCollapsible(humanizeKey(key), renderAny(val, depth + 1), depth < 2);
|
|
@@ -161,6 +256,7 @@ function renderAny(data: unknown, depth: number): string {
|
|
|
161
256
|
|
|
162
257
|
/** Render any JSON data as an HTML fragment. No business logic, pure structure. */
|
|
163
258
|
export function renderJSON(data: unknown): string {
|
|
259
|
+
tableCounter = 0; // Reset per render call
|
|
164
260
|
return `<div class="mcp-root">${renderAny(data, 0)}</div>`;
|
|
165
261
|
}
|
|
166
262
|
|
|
@@ -184,13 +280,14 @@ export function getRendererCSS(): string {
|
|
|
184
280
|
text-transform: uppercase; letter-spacing: 0.05em;
|
|
185
281
|
border-bottom: 2px solid var(--border);
|
|
186
282
|
white-space: nowrap; user-select: none;
|
|
283
|
+
position: sticky; top: 0; background: var(--bg-primary); z-index: 1;
|
|
187
284
|
}
|
|
188
285
|
.mcp-sortable { cursor: pointer; }
|
|
189
286
|
.mcp-sortable:hover { color: var(--accent); }
|
|
190
287
|
.mcp-sort-icon { margin-left: 4px; opacity: 0.3; font-size: 10px; }
|
|
191
288
|
.mcp-table td {
|
|
192
289
|
padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border);
|
|
193
|
-
vertical-align: top;
|
|
290
|
+
vertical-align: top; max-width: 400px;
|
|
194
291
|
}
|
|
195
292
|
.mcp-table tbody tr:hover { background: var(--accent-subtle); }
|
|
196
293
|
|
|
@@ -236,25 +333,42 @@ export function getRendererCSS(): string {
|
|
|
236
333
|
/* Primitives */
|
|
237
334
|
.mcp-null { color: var(--text-tertiary); font-style: italic; }
|
|
238
335
|
.mcp-bool { font-weight: 600; }
|
|
336
|
+
.mcp-bool-true { color: var(--success); }
|
|
337
|
+
.mcp-bool-false { color: var(--danger); }
|
|
239
338
|
.mcp-num { font-variant-numeric: tabular-nums; }
|
|
240
339
|
.mcp-text { white-space: pre-wrap; line-height: 1.6; }
|
|
340
|
+
.mcp-empty { color: var(--text-tertiary); font-style: italic; padding: var(--sp-2) 0; }
|
|
341
|
+
.mcp-truncated { color: var(--text-tertiary); font-style: italic; }
|
|
342
|
+
|
|
343
|
+
/* Links & images */
|
|
344
|
+
.mcp-link { color: var(--accent); text-decoration: none; word-break: break-all; }
|
|
345
|
+
.mcp-link:hover { text-decoration: underline; }
|
|
346
|
+
.mcp-img { max-width: 200px; max-height: 150px; border-radius: var(--radius-sm); border: 1px solid var(--border); }
|
|
347
|
+
|
|
348
|
+
/* Nested table styling */
|
|
349
|
+
.mcp-details .mcp-table-wrap { margin: 0; }
|
|
350
|
+
.mcp-details .mcp-table th { position: static; }
|
|
241
351
|
`;
|
|
242
352
|
}
|
|
243
353
|
|
|
244
|
-
/** Get the JS for table sorting (
|
|
354
|
+
/** Get the JS for table sorting (supports multiple tables) */
|
|
245
355
|
export function getRendererJS(): string {
|
|
246
356
|
return `
|
|
247
|
-
function __mcpSort(colIdx) {
|
|
248
|
-
var table = document.getElementById(
|
|
357
|
+
function __mcpSort(tableId, colIdx) {
|
|
358
|
+
var table = document.getElementById(tableId);
|
|
249
359
|
if (!table) return;
|
|
250
360
|
var tbody = table.tBodies[0];
|
|
251
361
|
var rows = Array.from(tbody.rows);
|
|
252
|
-
var
|
|
362
|
+
var key = tableId + '_' + colIdx;
|
|
363
|
+
var dir = table.dataset.sortKey === key && table.dataset.sortDir === 'asc' ? 'desc' : 'asc';
|
|
253
364
|
table.dataset.sortDir = dir;
|
|
365
|
+
table.dataset.sortKey = key;
|
|
254
366
|
|
|
255
367
|
rows.sort(function(a, b) {
|
|
256
|
-
var
|
|
257
|
-
|
|
368
|
+
var ac = a.cells[colIdx], bc = b.cells[colIdx];
|
|
369
|
+
if (!ac || !bc) return 0;
|
|
370
|
+
var av = ac.textContent.trim();
|
|
371
|
+
var bv = bc.textContent.trim();
|
|
258
372
|
var an = parseFloat(av.replace(/[^\\d.-]/g, ''));
|
|
259
373
|
var bn = parseFloat(bv.replace(/[^\\d.-]/g, ''));
|
|
260
374
|
if (!isNaN(an) && !isNaN(bn)) return dir === 'asc' ? an - bn : bn - an;
|
package/src/utilities.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind-like atomic utility classes.
|
|
3
|
+
*
|
|
4
|
+
* Maps familiar Tailwind class names to our CSS variable system.
|
|
5
|
+
* LLMs already know Tailwind syntax from training data — by providing
|
|
6
|
+
* these utilities, we ensure any model produces visually consistent HTML
|
|
7
|
+
* that automatically respects light/dark mode.
|
|
8
|
+
*
|
|
9
|
+
* This is NOT a full Tailwind build. It's a curated subset:
|
|
10
|
+
* - Layout (flex, grid, gap)
|
|
11
|
+
* - Spacing (p-*, m-*)
|
|
12
|
+
* - Typography (text-*, font-*)
|
|
13
|
+
* - Colors (text-*, bg-*, border-*)
|
|
14
|
+
* - Borders & radius
|
|
15
|
+
* - Shadows & effects
|
|
16
|
+
* - Sizing & overflow
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export function generateUtilityCSS(): string {
|
|
20
|
+
return `
|
|
21
|
+
/* ══════════════════════════════════════════
|
|
22
|
+
Tailwind-compatible utility classes
|
|
23
|
+
Mapped to CSS variable theming system
|
|
24
|
+
══════════════════════════════════════════ */
|
|
25
|
+
|
|
26
|
+
/* ── Display ── */
|
|
27
|
+
.block { display: block; }
|
|
28
|
+
.inline-block { display: inline-block; }
|
|
29
|
+
.inline { display: inline; }
|
|
30
|
+
.hidden { display: none; }
|
|
31
|
+
|
|
32
|
+
/* ── Flex ── */
|
|
33
|
+
.flex { display: flex; }
|
|
34
|
+
.inline-flex { display: inline-flex; }
|
|
35
|
+
.flex-row { flex-direction: row; }
|
|
36
|
+
.flex-col { flex-direction: column; }
|
|
37
|
+
.flex-wrap { flex-wrap: wrap; }
|
|
38
|
+
.flex-nowrap { flex-wrap: nowrap; }
|
|
39
|
+
.flex-1 { flex: 1 1 0%; }
|
|
40
|
+
.flex-auto { flex: 1 1 auto; }
|
|
41
|
+
.flex-none { flex: none; }
|
|
42
|
+
.items-start { align-items: flex-start; }
|
|
43
|
+
.items-center { align-items: center; }
|
|
44
|
+
.items-end { align-items: flex-end; }
|
|
45
|
+
.items-stretch { align-items: stretch; }
|
|
46
|
+
.items-baseline { align-items: baseline; }
|
|
47
|
+
.justify-start { justify-content: flex-start; }
|
|
48
|
+
.justify-center { justify-content: center; }
|
|
49
|
+
.justify-end { justify-content: flex-end; }
|
|
50
|
+
.justify-between { justify-content: space-between; }
|
|
51
|
+
.justify-around { justify-content: space-around; }
|
|
52
|
+
.justify-evenly { justify-content: space-evenly; }
|
|
53
|
+
.self-start { align-self: flex-start; }
|
|
54
|
+
.self-center { align-self: center; }
|
|
55
|
+
.self-end { align-self: flex-end; }
|
|
56
|
+
.shrink-0 { flex-shrink: 0; }
|
|
57
|
+
.grow { flex-grow: 1; }
|
|
58
|
+
|
|
59
|
+
/* ── Grid ── */
|
|
60
|
+
.grid { display: grid; }
|
|
61
|
+
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
62
|
+
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
63
|
+
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
64
|
+
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
65
|
+
.col-span-2 { grid-column: span 2 / span 2; }
|
|
66
|
+
.col-span-3 { grid-column: span 3 / span 3; }
|
|
67
|
+
.col-span-full { grid-column: 1 / -1; }
|
|
68
|
+
|
|
69
|
+
/* ── Gap ── */
|
|
70
|
+
.gap-1 { gap: var(--sp-1); }
|
|
71
|
+
.gap-2 { gap: var(--sp-2); }
|
|
72
|
+
.gap-3 { gap: var(--sp-3); }
|
|
73
|
+
.gap-4 { gap: var(--sp-4); }
|
|
74
|
+
.gap-5 { gap: var(--sp-5); }
|
|
75
|
+
.gap-6 { gap: var(--sp-6); }
|
|
76
|
+
.gap-8 { gap: var(--sp-8); }
|
|
77
|
+
|
|
78
|
+
/* ── Padding ── */
|
|
79
|
+
.p-0 { padding: 0; }
|
|
80
|
+
.p-1 { padding: var(--sp-1); }
|
|
81
|
+
.p-2 { padding: var(--sp-2); }
|
|
82
|
+
.p-3 { padding: var(--sp-3); }
|
|
83
|
+
.p-4 { padding: var(--sp-4); }
|
|
84
|
+
.p-5 { padding: var(--sp-5); }
|
|
85
|
+
.p-6 { padding: var(--sp-6); }
|
|
86
|
+
.p-8 { padding: var(--sp-8); }
|
|
87
|
+
.px-1 { padding-left: var(--sp-1); padding-right: var(--sp-1); }
|
|
88
|
+
.px-2 { padding-left: var(--sp-2); padding-right: var(--sp-2); }
|
|
89
|
+
.px-3 { padding-left: var(--sp-3); padding-right: var(--sp-3); }
|
|
90
|
+
.px-4 { padding-left: var(--sp-4); padding-right: var(--sp-4); }
|
|
91
|
+
.px-6 { padding-left: var(--sp-6); padding-right: var(--sp-6); }
|
|
92
|
+
.py-1 { padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
|
|
93
|
+
.py-2 { padding-top: var(--sp-2); padding-bottom: var(--sp-2); }
|
|
94
|
+
.py-3 { padding-top: var(--sp-3); padding-bottom: var(--sp-3); }
|
|
95
|
+
.py-4 { padding-top: var(--sp-4); padding-bottom: var(--sp-4); }
|
|
96
|
+
.py-6 { padding-top: var(--sp-6); padding-bottom: var(--sp-6); }
|
|
97
|
+
.pt-2 { padding-top: var(--sp-2); }
|
|
98
|
+
.pt-4 { padding-top: var(--sp-4); }
|
|
99
|
+
.pb-2 { padding-bottom: var(--sp-2); }
|
|
100
|
+
.pb-4 { padding-bottom: var(--sp-4); }
|
|
101
|
+
.pl-3 { padding-left: var(--sp-3); }
|
|
102
|
+
.pl-4 { padding-left: var(--sp-4); }
|
|
103
|
+
.pr-3 { padding-right: var(--sp-3); }
|
|
104
|
+
|
|
105
|
+
/* ── Margin ── */
|
|
106
|
+
.m-0 { margin: 0; }
|
|
107
|
+
.m-auto { margin: auto; }
|
|
108
|
+
.mx-auto { margin-left: auto; margin-right: auto; }
|
|
109
|
+
.mt-1 { margin-top: var(--sp-1); }
|
|
110
|
+
.mt-2 { margin-top: var(--sp-2); }
|
|
111
|
+
.mt-3 { margin-top: var(--sp-3); }
|
|
112
|
+
.mt-4 { margin-top: var(--sp-4); }
|
|
113
|
+
.mt-6 { margin-top: var(--sp-6); }
|
|
114
|
+
.mt-8 { margin-top: var(--sp-8); }
|
|
115
|
+
.mb-1 { margin-bottom: var(--sp-1); }
|
|
116
|
+
.mb-2 { margin-bottom: var(--sp-2); }
|
|
117
|
+
.mb-3 { margin-bottom: var(--sp-3); }
|
|
118
|
+
.mb-4 { margin-bottom: var(--sp-4); }
|
|
119
|
+
.mb-6 { margin-bottom: var(--sp-6); }
|
|
120
|
+
.ml-2 { margin-left: var(--sp-2); }
|
|
121
|
+
.ml-3 { margin-left: var(--sp-3); }
|
|
122
|
+
.mr-2 { margin-right: var(--sp-2); }
|
|
123
|
+
.mr-3 { margin-right: var(--sp-3); }
|
|
124
|
+
|
|
125
|
+
/* ── Width & Height ── */
|
|
126
|
+
.w-full { width: 100%; }
|
|
127
|
+
.w-auto { width: auto; }
|
|
128
|
+
.w-fit { width: fit-content; }
|
|
129
|
+
.max-w-sm { max-width: 24rem; }
|
|
130
|
+
.max-w-md { max-width: 28rem; }
|
|
131
|
+
.max-w-lg { max-width: 32rem; }
|
|
132
|
+
.max-w-xl { max-width: 36rem; }
|
|
133
|
+
.max-w-2xl { max-width: 42rem; }
|
|
134
|
+
.max-w-3xl { max-width: 48rem; }
|
|
135
|
+
.max-w-4xl { max-width: 56rem; }
|
|
136
|
+
.max-w-full { max-width: 100%; }
|
|
137
|
+
.max-w-prose { max-width: 65ch; }
|
|
138
|
+
.min-w-0 { min-width: 0; }
|
|
139
|
+
.h-auto { height: auto; }
|
|
140
|
+
.h-full { height: 100%; }
|
|
141
|
+
.min-h-0 { min-height: 0; }
|
|
142
|
+
|
|
143
|
+
/* ── Typography ── */
|
|
144
|
+
.text-xs { font-size: var(--text-xs); }
|
|
145
|
+
.text-sm { font-size: var(--text-sm); }
|
|
146
|
+
.text-base { font-size: var(--text-base); }
|
|
147
|
+
.text-lg { font-size: var(--text-lg); }
|
|
148
|
+
.text-xl { font-size: var(--text-xl); }
|
|
149
|
+
.text-2xl { font-size: var(--text-2xl); }
|
|
150
|
+
.text-3xl { font-size: var(--text-3xl); }
|
|
151
|
+
.font-sans { font-family: var(--font-sans); }
|
|
152
|
+
.font-mono { font-family: var(--font-mono); }
|
|
153
|
+
.font-normal { font-weight: 400; }
|
|
154
|
+
.font-medium { font-weight: 500; }
|
|
155
|
+
.font-semibold { font-weight: 600; }
|
|
156
|
+
.font-bold { font-weight: 700; }
|
|
157
|
+
.italic { font-style: italic; }
|
|
158
|
+
.not-italic { font-style: normal; }
|
|
159
|
+
.leading-none { line-height: 1; }
|
|
160
|
+
.leading-tight { line-height: 1.25; }
|
|
161
|
+
.leading-snug { line-height: 1.375; }
|
|
162
|
+
.leading-normal { line-height: 1.5; }
|
|
163
|
+
.leading-relaxed { line-height: 1.625; }
|
|
164
|
+
.tracking-tight { letter-spacing: -0.025em; }
|
|
165
|
+
.tracking-normal { letter-spacing: 0; }
|
|
166
|
+
.tracking-wide { letter-spacing: 0.025em; }
|
|
167
|
+
.tracking-wider { letter-spacing: 0.05em; }
|
|
168
|
+
.uppercase { text-transform: uppercase; }
|
|
169
|
+
.lowercase { text-transform: lowercase; }
|
|
170
|
+
.capitalize { text-transform: capitalize; }
|
|
171
|
+
.normal-case { text-transform: none; }
|
|
172
|
+
.text-left { text-align: left; }
|
|
173
|
+
.text-center { text-align: center; }
|
|
174
|
+
.text-right { text-align: right; }
|
|
175
|
+
.underline { text-decoration: underline; }
|
|
176
|
+
.no-underline { text-decoration: none; }
|
|
177
|
+
.line-through { text-decoration: line-through; }
|
|
178
|
+
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
179
|
+
.whitespace-pre { white-space: pre; }
|
|
180
|
+
.whitespace-pre-wrap { white-space: pre-wrap; }
|
|
181
|
+
.whitespace-nowrap { white-space: nowrap; }
|
|
182
|
+
.break-words { word-break: break-word; overflow-wrap: break-word; }
|
|
183
|
+
.break-all { word-break: break-all; }
|
|
184
|
+
.tabular-nums { font-variant-numeric: tabular-nums; }
|
|
185
|
+
|
|
186
|
+
/* ── Text Colors ── */
|
|
187
|
+
.text-primary { color: var(--text-primary); }
|
|
188
|
+
.text-secondary { color: var(--text-secondary); }
|
|
189
|
+
.text-tertiary { color: var(--text-tertiary); }
|
|
190
|
+
.text-inverse { color: var(--text-inverse); }
|
|
191
|
+
.text-accent { color: var(--accent); }
|
|
192
|
+
.text-success { color: var(--success); }
|
|
193
|
+
.text-warning { color: var(--warning); }
|
|
194
|
+
.text-danger { color: var(--danger); }
|
|
195
|
+
.text-info { color: var(--info); }
|
|
196
|
+
|
|
197
|
+
/* ── Background Colors ── */
|
|
198
|
+
.bg-primary { background-color: var(--bg-primary); }
|
|
199
|
+
.bg-secondary { background-color: var(--bg-secondary); }
|
|
200
|
+
.bg-tertiary { background-color: var(--bg-tertiary); }
|
|
201
|
+
.bg-elevated { background-color: var(--bg-elevated); }
|
|
202
|
+
.bg-accent { background-color: var(--accent); }
|
|
203
|
+
.bg-accent-subtle { background-color: var(--accent-subtle); }
|
|
204
|
+
.bg-success { background-color: var(--success); }
|
|
205
|
+
.bg-success-subtle { background-color: var(--success-subtle); }
|
|
206
|
+
.bg-warning { background-color: var(--warning); }
|
|
207
|
+
.bg-warning-subtle { background-color: var(--warning-subtle); }
|
|
208
|
+
.bg-danger { background-color: var(--danger); }
|
|
209
|
+
.bg-danger-subtle { background-color: var(--danger-subtle); }
|
|
210
|
+
.bg-info { background-color: var(--info); }
|
|
211
|
+
.bg-info-subtle { background-color: var(--info-subtle); }
|
|
212
|
+
.bg-transparent { background-color: transparent; }
|
|
213
|
+
|
|
214
|
+
/* ── Borders ── */
|
|
215
|
+
.border { border: 1px solid var(--border); }
|
|
216
|
+
.border-2 { border: 2px solid var(--border); }
|
|
217
|
+
.border-t { border-top: 1px solid var(--border); }
|
|
218
|
+
.border-b { border-bottom: 1px solid var(--border); }
|
|
219
|
+
.border-l { border-left: 1px solid var(--border); }
|
|
220
|
+
.border-r { border-right: 1px solid var(--border); }
|
|
221
|
+
.border-strong { border-color: var(--border-strong); }
|
|
222
|
+
.border-accent { border-color: var(--accent); }
|
|
223
|
+
.border-success { border-color: var(--success); }
|
|
224
|
+
.border-warning { border-color: var(--warning); }
|
|
225
|
+
.border-danger { border-color: var(--danger); }
|
|
226
|
+
.border-transparent { border-color: transparent; }
|
|
227
|
+
.border-none { border: none; }
|
|
228
|
+
|
|
229
|
+
/* ── Border Radius ── */
|
|
230
|
+
.rounded-none { border-radius: 0; }
|
|
231
|
+
.rounded-sm { border-radius: var(--radius-sm); }
|
|
232
|
+
.rounded { border-radius: var(--radius-md); }
|
|
233
|
+
.rounded-md { border-radius: var(--radius-md); }
|
|
234
|
+
.rounded-lg { border-radius: var(--radius-lg); }
|
|
235
|
+
.rounded-full { border-radius: var(--radius-full); }
|
|
236
|
+
|
|
237
|
+
/* ── Shadows ── */
|
|
238
|
+
.shadow-sm { box-shadow: var(--shadow-sm); }
|
|
239
|
+
.shadow { box-shadow: var(--shadow-md); }
|
|
240
|
+
.shadow-md { box-shadow: var(--shadow-md); }
|
|
241
|
+
.shadow-lg { box-shadow: var(--shadow-lg); }
|
|
242
|
+
.shadow-none { box-shadow: none; }
|
|
243
|
+
|
|
244
|
+
/* ── Overflow ── */
|
|
245
|
+
.overflow-auto { overflow: auto; }
|
|
246
|
+
.overflow-hidden { overflow: hidden; }
|
|
247
|
+
.overflow-x-auto { overflow-x: auto; }
|
|
248
|
+
.overflow-y-auto { overflow-y: auto; }
|
|
249
|
+
.overflow-scroll { overflow: scroll; }
|
|
250
|
+
|
|
251
|
+
/* ── Position ── */
|
|
252
|
+
.relative { position: relative; }
|
|
253
|
+
.absolute { position: absolute; }
|
|
254
|
+
.fixed { position: fixed; }
|
|
255
|
+
.sticky { position: sticky; }
|
|
256
|
+
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
|
|
257
|
+
.top-0 { top: 0; }
|
|
258
|
+
.right-0 { right: 0; }
|
|
259
|
+
.bottom-0 { bottom: 0; }
|
|
260
|
+
.left-0 { left: 0; }
|
|
261
|
+
|
|
262
|
+
/* ── Z-Index ── */
|
|
263
|
+
.z-0 { z-index: 0; }
|
|
264
|
+
.z-10 { z-index: 10; }
|
|
265
|
+
.z-20 { z-index: 20; }
|
|
266
|
+
.z-50 { z-index: 50; }
|
|
267
|
+
|
|
268
|
+
/* ── Opacity ── */
|
|
269
|
+
.opacity-0 { opacity: 0; }
|
|
270
|
+
.opacity-25 { opacity: 0.25; }
|
|
271
|
+
.opacity-50 { opacity: 0.5; }
|
|
272
|
+
.opacity-75 { opacity: 0.75; }
|
|
273
|
+
.opacity-100 { opacity: 1; }
|
|
274
|
+
|
|
275
|
+
/* ── Cursor ── */
|
|
276
|
+
.cursor-pointer { cursor: pointer; }
|
|
277
|
+
.cursor-default { cursor: default; }
|
|
278
|
+
.select-none { user-select: none; }
|
|
279
|
+
|
|
280
|
+
/* ── Transitions ── */
|
|
281
|
+
.transition { transition: all var(--duration-normal) var(--ease-out); }
|
|
282
|
+
.transition-colors { transition: color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out); }
|
|
283
|
+
|
|
284
|
+
/* ── List ── */
|
|
285
|
+
.list-none { list-style: none; }
|
|
286
|
+
.list-disc { list-style-type: disc; }
|
|
287
|
+
.list-decimal { list-style-type: decimal; }
|
|
288
|
+
.list-inside { list-style-position: inside; }
|
|
289
|
+
|
|
290
|
+
/* ── Table ── */
|
|
291
|
+
.table { display: table; }
|
|
292
|
+
.table-auto { table-layout: auto; }
|
|
293
|
+
.table-fixed { table-layout: fixed; }
|
|
294
|
+
.border-collapse { border-collapse: collapse; }
|
|
295
|
+
|
|
296
|
+
/* ── SVG ── */
|
|
297
|
+
.fill-current { fill: currentColor; }
|
|
298
|
+
.stroke-current { stroke: currentColor; }
|
|
299
|
+
|
|
300
|
+
/* ── Responsive ── */
|
|
301
|
+
@media (min-width: 640px) {
|
|
302
|
+
.sm\\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
303
|
+
.sm\\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
304
|
+
.sm\\:flex-row { flex-direction: row; }
|
|
305
|
+
.sm\\:text-lg { font-size: var(--text-lg); }
|
|
306
|
+
.sm\\:text-xl { font-size: var(--text-xl); }
|
|
307
|
+
.sm\\:p-6 { padding: var(--sp-6); }
|
|
308
|
+
}
|
|
309
|
+
@media (min-width: 768px) {
|
|
310
|
+
.md\\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
311
|
+
.md\\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
312
|
+
.md\\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
313
|
+
.md\\:flex-row { flex-direction: row; }
|
|
314
|
+
.md\\:text-2xl { font-size: var(--text-2xl); }
|
|
315
|
+
.md\\:text-3xl { font-size: var(--text-3xl); }
|
|
316
|
+
.md\\:p-8 { padding: var(--sp-8); }
|
|
317
|
+
}
|
|
318
|
+
`;
|
|
319
|
+
}
|