@runsnative/mcp-server 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.
- package/README.md +96 -0
- package/dist/content.js +324 -0
- package/dist/http-server.js +213 -0
- package/dist/index.js +72 -0
- package/dist/inferrer/image-extractor.js +76 -0
- package/dist/inferrer/page-fetcher.js +46 -0
- package/dist/inferrer/pipeline.js +27 -0
- package/dist/inferrer/style-extractor.js +75 -0
- package/dist/inferrer/theme-inferrer.js +134 -0
- package/dist/local-provider.js +38 -0
- package/dist/provider.js +27 -0
- package/dist/remote-provider.js +339 -0
- package/dist/test/validate.js +315 -0
- package/dist/tools/get-component.js +47 -0
- package/dist/tools/get-composition-pattern.js +43 -0
- package/dist/tools/get-foundation.js +38 -0
- package/dist/tools/get-step.js +44 -0
- package/dist/tools/infer-brand-theme.js +29 -0
- package/dist/tools/infer-theme.js +95 -0
- package/dist/tools/list-components.js +35 -0
- package/dist/tools/list-composition-patterns.js +29 -0
- package/dist/tools/list-exercises.js +23 -0
- package/dist/tools/search-docs.js +43 -0
- package/dist/tools/start-exercise.js +36 -0
- package/dist/tools/surface-preview.js +36 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { GET_COMPONENT_TOOL, handleGetComponent } from './tools/get-component.js';
|
|
5
|
+
import { LIST_COMPONENTS_TOOL, handleListComponents } from './tools/list-components.js';
|
|
6
|
+
import { GET_FOUNDATION_TOOL, handleGetFoundation } from './tools/get-foundation.js';
|
|
7
|
+
import { SEARCH_DOCS_TOOL, handleSearchDocs } from './tools/search-docs.js';
|
|
8
|
+
import { INFER_THEME_TOOL, handleInferTheme } from './tools/infer-theme.js';
|
|
9
|
+
import { INFER_BRAND_THEME_TOOL, handleInferBrandTheme } from './tools/infer-brand-theme.js';
|
|
10
|
+
import { LIST_EXERCISES_TOOL, handleListExercises } from './tools/list-exercises.js';
|
|
11
|
+
import { START_EXERCISE_TOOL, handleStartExercise } from './tools/start-exercise.js';
|
|
12
|
+
import { GET_STEP_TOOL, handleGetStep } from './tools/get-step.js';
|
|
13
|
+
import { SURFACE_PREVIEW_TOOL, handleSurfacePreview } from './tools/surface-preview.js';
|
|
14
|
+
import { LIST_COMPOSITION_PATTERNS_TOOL, handleListCompositionPatterns } from './tools/list-composition-patterns.js';
|
|
15
|
+
import { GET_COMPOSITION_PATTERN_TOOL, handleGetCompositionPattern } from './tools/get-composition-pattern.js';
|
|
16
|
+
import { createProvider } from './provider.js';
|
|
17
|
+
import { RemoteContentProvider } from './remote-provider.js';
|
|
18
|
+
// Create the content provider — LocalContentProvider if content tree is present,
|
|
19
|
+
// RemoteContentProvider otherwise. Initialize runs the manifest version check.
|
|
20
|
+
const provider = await createProvider();
|
|
21
|
+
if (provider instanceof RemoteContentProvider) {
|
|
22
|
+
await provider.initialize();
|
|
23
|
+
}
|
|
24
|
+
const server = new Server({ name: 'runsnative-mcp', version: '0.3.0' }, { capabilities: { tools: {} } });
|
|
25
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
26
|
+
tools: [
|
|
27
|
+
GET_COMPONENT_TOOL,
|
|
28
|
+
LIST_COMPONENTS_TOOL,
|
|
29
|
+
GET_FOUNDATION_TOOL,
|
|
30
|
+
SEARCH_DOCS_TOOL,
|
|
31
|
+
INFER_THEME_TOOL,
|
|
32
|
+
INFER_BRAND_THEME_TOOL,
|
|
33
|
+
LIST_EXERCISES_TOOL,
|
|
34
|
+
START_EXERCISE_TOOL,
|
|
35
|
+
GET_STEP_TOOL,
|
|
36
|
+
SURFACE_PREVIEW_TOOL,
|
|
37
|
+
LIST_COMPOSITION_PATTERNS_TOOL,
|
|
38
|
+
GET_COMPOSITION_PATTERN_TOOL,
|
|
39
|
+
],
|
|
40
|
+
}));
|
|
41
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
42
|
+
switch (request.params.name) {
|
|
43
|
+
case 'get_component':
|
|
44
|
+
return handleGetComponent(provider, request.params.arguments ?? {});
|
|
45
|
+
case 'list_components':
|
|
46
|
+
return handleListComponents(provider, request.params.arguments ?? {});
|
|
47
|
+
case 'get_foundation':
|
|
48
|
+
return handleGetFoundation(provider, request.params.arguments ?? {});
|
|
49
|
+
case 'search_docs':
|
|
50
|
+
return handleSearchDocs(provider, request.params.arguments ?? {});
|
|
51
|
+
case 'infer_theme':
|
|
52
|
+
return handleInferTheme(request.params.arguments ?? {});
|
|
53
|
+
case 'infer_brand_theme':
|
|
54
|
+
return handleInferBrandTheme(request.params.arguments ?? {});
|
|
55
|
+
case 'list_exercises':
|
|
56
|
+
return handleListExercises(provider, request.params.arguments ?? {});
|
|
57
|
+
case 'start_exercise':
|
|
58
|
+
return handleStartExercise(provider, request.params.arguments ?? {});
|
|
59
|
+
case 'get_step':
|
|
60
|
+
return handleGetStep(provider, request.params.arguments ?? {});
|
|
61
|
+
case 'surface_preview':
|
|
62
|
+
return handleSurfacePreview();
|
|
63
|
+
case 'list_composition_patterns':
|
|
64
|
+
return handleListCompositionPatterns(provider, request.params.arguments ?? {});
|
|
65
|
+
case 'get_composition_pattern':
|
|
66
|
+
return handleGetCompositionPattern(provider, request.params.arguments ?? {});
|
|
67
|
+
default:
|
|
68
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const transport = new StdioServerTransport();
|
|
72
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
const MAX_PIXELS = 10_000;
|
|
3
|
+
const MAX_ITER = 20;
|
|
4
|
+
function toHex(r, g, b) {
|
|
5
|
+
return '#' + [r, g, b]
|
|
6
|
+
.map(v => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0'))
|
|
7
|
+
.join('');
|
|
8
|
+
}
|
|
9
|
+
function sqDist(ar, ag, ab, br, bg, bb) {
|
|
10
|
+
return (ar - br) ** 2 + (ag - bg) ** 2 + (ab - bb) ** 2;
|
|
11
|
+
}
|
|
12
|
+
export async function extractImageColors(buffer, sourceType, k = 5) {
|
|
13
|
+
const extractedAt = new Date().toISOString();
|
|
14
|
+
const { data, info } = await sharp(buffer)
|
|
15
|
+
.flatten({ background: '#ffffff' })
|
|
16
|
+
.removeAlpha()
|
|
17
|
+
.raw()
|
|
18
|
+
.toBuffer({ resolveWithObject: true });
|
|
19
|
+
const totalPixels = info.width * info.height;
|
|
20
|
+
const stride = Math.max(1, Math.floor(totalPixels / MAX_PIXELS));
|
|
21
|
+
const pixels = [];
|
|
22
|
+
for (let i = 0; i < data.length; i += stride * 3) {
|
|
23
|
+
pixels.push([data[i], data[i + 1], data[i + 2]]);
|
|
24
|
+
}
|
|
25
|
+
// k-means++ initialisation
|
|
26
|
+
const centroids = [];
|
|
27
|
+
centroids.push([...pixels[Math.floor(Math.random() * pixels.length)]]);
|
|
28
|
+
for (let c = 1; c < k; c++) {
|
|
29
|
+
const dists = pixels.map(p => Math.min(...centroids.map(cen => sqDist(p[0], p[1], p[2], cen[0], cen[1], cen[2]))));
|
|
30
|
+
const total = dists.reduce((s, d) => s + d, 0);
|
|
31
|
+
let rnd = Math.random() * total;
|
|
32
|
+
let chosen = pixels.length - 1;
|
|
33
|
+
for (let i = 0; i < dists.length; i++) {
|
|
34
|
+
rnd -= dists[i];
|
|
35
|
+
if (rnd <= 0) {
|
|
36
|
+
chosen = i;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
centroids.push([...pixels[chosen]]);
|
|
41
|
+
}
|
|
42
|
+
// Lloyd's iterations
|
|
43
|
+
let assignments = new Array(pixels.length).fill(0);
|
|
44
|
+
for (let iter = 0; iter < MAX_ITER; iter++) {
|
|
45
|
+
const next = pixels.map(p => centroids.reduce((best, cen, ci) => {
|
|
46
|
+
const d = sqDist(p[0], p[1], p[2], cen[0], cen[1], cen[2]);
|
|
47
|
+
return d < best.d ? { ci, d } : best;
|
|
48
|
+
}, { ci: 0, d: Infinity }).ci);
|
|
49
|
+
const sums = centroids.map(() => [0, 0, 0]);
|
|
50
|
+
const counts = new Array(k).fill(0);
|
|
51
|
+
next.forEach((ci, pi) => {
|
|
52
|
+
sums[ci][0] += pixels[pi][0];
|
|
53
|
+
sums[ci][1] += pixels[pi][1];
|
|
54
|
+
sums[ci][2] += pixels[pi][2];
|
|
55
|
+
counts[ci]++;
|
|
56
|
+
});
|
|
57
|
+
centroids.forEach((cen, ci) => {
|
|
58
|
+
if (counts[ci] > 0) {
|
|
59
|
+
cen[0] = Math.round(sums[ci][0] / counts[ci]);
|
|
60
|
+
cen[1] = Math.round(sums[ci][1] / counts[ci]);
|
|
61
|
+
cen[2] = Math.round(sums[ci][2] / counts[ci]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const converged = next.every((ci, i) => ci === assignments[i]);
|
|
65
|
+
assignments = next;
|
|
66
|
+
if (converged)
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const counts = new Array(k).fill(0);
|
|
70
|
+
assignments.forEach(ci => counts[ci]++);
|
|
71
|
+
const dominantColors = centroids
|
|
72
|
+
.map((cen, ci) => ({ hex: toHex(cen[0], cen[1], cen[2]), frequency: counts[ci] / pixels.length }))
|
|
73
|
+
.filter(dc => dc.frequency > 0)
|
|
74
|
+
.sort((a, b) => b.frequency - a.frequency);
|
|
75
|
+
return { dominantColors, sourceType, extractedAt };
|
|
76
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
const VIEWPORT = { width: 1280, height: 800 };
|
|
3
|
+
export async function fetchAndRender(url) {
|
|
4
|
+
let parsed;
|
|
5
|
+
try {
|
|
6
|
+
parsed = new URL(url);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return { ok: false, error: { code: 'FETCH_ERROR', message: 'Invalid URL', url } };
|
|
10
|
+
}
|
|
11
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
12
|
+
return { ok: false, error: { code: 'FETCH_ERROR', message: `Unsupported scheme: ${parsed.protocol}`, url } };
|
|
13
|
+
}
|
|
14
|
+
const browser = await chromium.launch({ headless: true });
|
|
15
|
+
try {
|
|
16
|
+
const page = await browser.newPage();
|
|
17
|
+
await page.setViewportSize(VIEWPORT);
|
|
18
|
+
const response = await page.goto(url, { waitUntil: 'networkidle' });
|
|
19
|
+
if (!response) {
|
|
20
|
+
return { ok: false, error: { code: 'FETCH_ERROR', message: 'Navigation returned no response', url } };
|
|
21
|
+
}
|
|
22
|
+
const htmlSource = await response.text();
|
|
23
|
+
const renderedHtml = await page.content();
|
|
24
|
+
const screenshot = await page.screenshot({ fullPage: true });
|
|
25
|
+
const title = await page.title();
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
snapshot: {
|
|
29
|
+
url: page.url(),
|
|
30
|
+
htmlSource,
|
|
31
|
+
renderedHtml,
|
|
32
|
+
screenshot,
|
|
33
|
+
viewport: VIEWPORT,
|
|
34
|
+
title,
|
|
35
|
+
fetchedAt: new Date().toISOString(),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
41
|
+
return { ok: false, error: { code: 'FETCH_ERROR', message: 'Headless render failed', url, cause } };
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await browser.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { fetchAndRender } from './page-fetcher.js';
|
|
2
|
+
import { extractStyles } from './style-extractor.js';
|
|
3
|
+
import { extractImageColors } from './image-extractor.js';
|
|
4
|
+
import { inferTheme } from './theme-inferrer.js';
|
|
5
|
+
export async function runInferencePipeline(url) {
|
|
6
|
+
const fetchResult = await fetchAndRender(url);
|
|
7
|
+
if (!fetchResult.ok)
|
|
8
|
+
return { ok: false, error: fetchResult.error };
|
|
9
|
+
const { snapshot } = fetchResult;
|
|
10
|
+
let imageEvidence = undefined;
|
|
11
|
+
try {
|
|
12
|
+
imageEvidence = await extractImageColors(snapshot.screenshot, 'screenshot');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Image extraction is best-effort — proceed without it.
|
|
16
|
+
}
|
|
17
|
+
let evidence;
|
|
18
|
+
try {
|
|
19
|
+
evidence = await extractStyles(snapshot);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
23
|
+
return { ok: false, error: { code: 'EXTRACTION_ERROR', message: 'Style extraction failed', url: snapshot.url, cause } };
|
|
24
|
+
}
|
|
25
|
+
const result = inferTheme(evidence, imageEvidence);
|
|
26
|
+
return { ok: true, result };
|
|
27
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
const SAMPLE_SELECTORS = 'body,h1,h2,h3,h4,h5,h6,p,a,button,input,select,textarea,' +
|
|
3
|
+
'div,span,header,footer,nav,main,section,article,aside,ul,ol,li';
|
|
4
|
+
const MAX_ELEMENTS = 200;
|
|
5
|
+
export async function extractStyles(snapshot) {
|
|
6
|
+
const browser = await chromium.launch({ headless: true });
|
|
7
|
+
try {
|
|
8
|
+
const page = await browser.newPage();
|
|
9
|
+
await page.setViewportSize({ width: 1280, height: 800 });
|
|
10
|
+
await page.setContent(snapshot.renderedHtml, { waitUntil: 'domcontentloaded' });
|
|
11
|
+
const raw = await page.evaluate(({ selectors, maxElements }) => {
|
|
12
|
+
const elements = Array.from(document.querySelectorAll(selectors)).slice(0, maxElements);
|
|
13
|
+
const colors = new Set();
|
|
14
|
+
const fontFamilies = new Set();
|
|
15
|
+
const fontSizes = new Set();
|
|
16
|
+
const borderRadii = new Set();
|
|
17
|
+
const shadows = new Set();
|
|
18
|
+
const spacings = new Set();
|
|
19
|
+
for (const el of elements) {
|
|
20
|
+
const cs = window.getComputedStyle(el);
|
|
21
|
+
for (const prop of [
|
|
22
|
+
'color', 'background-color', 'border-top-color',
|
|
23
|
+
'border-right-color', 'border-bottom-color', 'border-left-color', 'outline-color',
|
|
24
|
+
]) {
|
|
25
|
+
const v = cs.getPropertyValue(prop);
|
|
26
|
+
if (v && v !== 'rgba(0, 0, 0, 0)')
|
|
27
|
+
colors.add(v);
|
|
28
|
+
}
|
|
29
|
+
const ff = cs.getPropertyValue('font-family');
|
|
30
|
+
if (ff)
|
|
31
|
+
fontFamilies.add(ff);
|
|
32
|
+
const fs = cs.getPropertyValue('font-size');
|
|
33
|
+
if (fs && fs !== '0px')
|
|
34
|
+
fontSizes.add(fs);
|
|
35
|
+
for (const prop of [
|
|
36
|
+
'border-top-left-radius', 'border-top-right-radius',
|
|
37
|
+
'border-bottom-right-radius', 'border-bottom-left-radius',
|
|
38
|
+
]) {
|
|
39
|
+
const v = cs.getPropertyValue(prop);
|
|
40
|
+
if (v && v !== '0px')
|
|
41
|
+
borderRadii.add(v);
|
|
42
|
+
}
|
|
43
|
+
for (const prop of ['box-shadow', 'text-shadow']) {
|
|
44
|
+
const v = cs.getPropertyValue(prop);
|
|
45
|
+
if (v && v !== 'none')
|
|
46
|
+
shadows.add(v);
|
|
47
|
+
}
|
|
48
|
+
for (const prop of [
|
|
49
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
50
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
51
|
+
]) {
|
|
52
|
+
const v = cs.getPropertyValue(prop);
|
|
53
|
+
if (v && v !== '0px')
|
|
54
|
+
spacings.add(v);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
colors: [...colors],
|
|
59
|
+
fontFamilies: [...fontFamilies],
|
|
60
|
+
fontSizes: [...fontSizes],
|
|
61
|
+
borderRadii: [...borderRadii],
|
|
62
|
+
shadows: [...shadows],
|
|
63
|
+
spacings: [...spacings],
|
|
64
|
+
};
|
|
65
|
+
}, { selectors: SAMPLE_SELECTORS, maxElements: MAX_ELEMENTS });
|
|
66
|
+
return {
|
|
67
|
+
...raw,
|
|
68
|
+
sourceUrl: snapshot.url,
|
|
69
|
+
extractedAt: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await browser.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
function parseCssColor(css) {
|
|
2
|
+
const m = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(css);
|
|
3
|
+
if (!m)
|
|
4
|
+
return null;
|
|
5
|
+
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
|
|
6
|
+
}
|
|
7
|
+
function rgbToHex(r, g, b) {
|
|
8
|
+
const ch = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0');
|
|
9
|
+
return `#${ch(r)}${ch(g)}${ch(b)}`;
|
|
10
|
+
}
|
|
11
|
+
function rgbRange(r, g, b) {
|
|
12
|
+
return Math.max(r, g, b) - Math.min(r, g, b);
|
|
13
|
+
}
|
|
14
|
+
function parsePx(value) {
|
|
15
|
+
const m = /^([\d.]+)px$/.exec(value.trim());
|
|
16
|
+
return m ? parseFloat(m[1]) : null;
|
|
17
|
+
}
|
|
18
|
+
function median(values) {
|
|
19
|
+
if (values.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
22
|
+
const mid = Math.floor(sorted.length / 2);
|
|
23
|
+
return sorted.length % 2 === 0
|
|
24
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
25
|
+
: sorted[mid];
|
|
26
|
+
}
|
|
27
|
+
function inferPrimaryColor(evidence, imageEvidence) {
|
|
28
|
+
if (imageEvidence && imageEvidence.dominantColors.length > 0) {
|
|
29
|
+
return { value: imageEvidence.dominantColors[0].hex, confidence: 'high' };
|
|
30
|
+
}
|
|
31
|
+
let bestHex = '';
|
|
32
|
+
let bestRange = -1;
|
|
33
|
+
for (const css of evidence.colors) {
|
|
34
|
+
const rgb = parseCssColor(css);
|
|
35
|
+
if (!rgb)
|
|
36
|
+
continue;
|
|
37
|
+
const range = rgbRange(rgb[0], rgb[1], rgb[2]);
|
|
38
|
+
if (range > bestRange) {
|
|
39
|
+
bestRange = range;
|
|
40
|
+
bestHex = rgbToHex(rgb[0], rgb[1], rgb[2]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (bestHex) {
|
|
44
|
+
return { value: bestHex, confidence: bestRange >= 60 ? 'high' : 'medium' };
|
|
45
|
+
}
|
|
46
|
+
return { value: '#3b82f6', confidence: 'low' };
|
|
47
|
+
}
|
|
48
|
+
function inferTypography(fontFamilies) {
|
|
49
|
+
const haystack = fontFamilies.join(' ').toLowerCase();
|
|
50
|
+
if (!haystack.trim())
|
|
51
|
+
return { value: 'modern', confidence: 'low' };
|
|
52
|
+
const rules = [
|
|
53
|
+
[/mono|monospace|courier|consolas|source code|fira code|jetbrains|inconsolata/, 'technical'],
|
|
54
|
+
[/impact|black|ultra|extrabold|heavy|oswald|bebas|druk/, 'bold'],
|
|
55
|
+
[/playfair|freight|tiempos|canela|gt alpina|chronicle|le monde/, 'editorial'],
|
|
56
|
+
[/times|georgia|palatino|garamond|caslon|baskerville|bodoni|didot/, 'classic'],
|
|
57
|
+
[/rounded|nunito|quicksand|varela|comfortaa|dosis/, 'friendly'],
|
|
58
|
+
[/futura|circular|poppins|brandon|montserrat|raleway|proxima nova/, 'geometric'],
|
|
59
|
+
[/merriweather|lora|ibm plex|source serif|humanist|charter|utopia/, 'humanist'],
|
|
60
|
+
[/helvetica|avenir|gill sans|calibri|myriad|aktiv|graphik|haas/, 'minimal'],
|
|
61
|
+
[/corporate|arial|verdana|trebuchet/, 'corporate'],
|
|
62
|
+
[/inter|roboto|system-ui|segoe|sf pro|apple system|blinkmacsystem|-apple-system/, 'modern'],
|
|
63
|
+
];
|
|
64
|
+
for (const [pattern, preset] of rules) {
|
|
65
|
+
if (pattern.test(haystack))
|
|
66
|
+
return { value: preset, confidence: 'medium' };
|
|
67
|
+
}
|
|
68
|
+
return { value: 'modern', confidence: 'low' };
|
|
69
|
+
}
|
|
70
|
+
function inferBorderRadius(borderRadii) {
|
|
71
|
+
const pxValues = borderRadii.map(parsePx).filter((v) => v !== null);
|
|
72
|
+
if (pxValues.length === 0)
|
|
73
|
+
return { value: 'sharp', confidence: 'low' };
|
|
74
|
+
const max = Math.max(...pxValues);
|
|
75
|
+
if (max <= 3)
|
|
76
|
+
return { value: 'sharp', confidence: 'high' };
|
|
77
|
+
if (max <= 8)
|
|
78
|
+
return { value: 'subtle', confidence: 'high' };
|
|
79
|
+
if (max <= 24)
|
|
80
|
+
return { value: 'rounded', confidence: 'high' };
|
|
81
|
+
return { value: 'pill', confidence: 'high' };
|
|
82
|
+
}
|
|
83
|
+
function inferElevation(shadows) {
|
|
84
|
+
const count = shadows.length;
|
|
85
|
+
if (count === 0)
|
|
86
|
+
return { value: 'flat', confidence: 'high' };
|
|
87
|
+
if (count <= 2)
|
|
88
|
+
return { value: 'subtle', confidence: 'high' };
|
|
89
|
+
if (count <= 5)
|
|
90
|
+
return { value: 'medium', confidence: 'medium' };
|
|
91
|
+
return { value: 'dramatic', confidence: 'medium' };
|
|
92
|
+
}
|
|
93
|
+
function inferDensity(spacings) {
|
|
94
|
+
const pxValues = spacings.map(parsePx).filter((v) => v !== null);
|
|
95
|
+
const med = median(pxValues);
|
|
96
|
+
if (med === null)
|
|
97
|
+
return { value: 'default', confidence: 'low' };
|
|
98
|
+
if (med < 8)
|
|
99
|
+
return { value: 'compact', confidence: 'medium' };
|
|
100
|
+
if (med < 16)
|
|
101
|
+
return { value: 'default', confidence: 'medium' };
|
|
102
|
+
if (med < 24)
|
|
103
|
+
return { value: 'comfortable', confidence: 'medium' };
|
|
104
|
+
return { value: 'spacious', confidence: 'medium' };
|
|
105
|
+
}
|
|
106
|
+
export function inferTheme(evidence, imageEvidence) {
|
|
107
|
+
const primary = inferPrimaryColor(evidence, imageEvidence);
|
|
108
|
+
const typography = inferTypography(evidence.fontFamilies);
|
|
109
|
+
const radius = inferBorderRadius(evidence.borderRadii);
|
|
110
|
+
const elevation = inferElevation(evidence.shadows);
|
|
111
|
+
const density = inferDensity(evidence.spacings);
|
|
112
|
+
return {
|
|
113
|
+
theme: {
|
|
114
|
+
primaryColor: primary.value,
|
|
115
|
+
secondaryColor: 'auto',
|
|
116
|
+
typography: typography.value,
|
|
117
|
+
density: density.value,
|
|
118
|
+
borderRadius: radius.value,
|
|
119
|
+
elevation: elevation.value,
|
|
120
|
+
motion: 'smooth',
|
|
121
|
+
},
|
|
122
|
+
confidence: {
|
|
123
|
+
primaryColor: primary.confidence,
|
|
124
|
+
secondaryColor: 'medium',
|
|
125
|
+
typography: typography.confidence,
|
|
126
|
+
density: density.confidence,
|
|
127
|
+
borderRadius: radius.confidence,
|
|
128
|
+
elevation: elevation.confidence,
|
|
129
|
+
motion: 'low',
|
|
130
|
+
},
|
|
131
|
+
sourceUrl: evidence.sourceUrl,
|
|
132
|
+
inferredAt: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getComponent, listComponentMeta, getFoundation, listFoundations, searchDocs, listExerciseMeta, getExerciseDetail, getExerciseStep, listRecipeMeta, getRecipe, } from './content.js';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// LocalContentProvider — reads directly from the filesystem content tree.
|
|
4
|
+
// Used when running inside the runsnative repo (dev path) or when
|
|
5
|
+
// RUNSNATIVE_CONTENT_ROOT is set explicitly.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export class LocalContentProvider {
|
|
8
|
+
getComponent(name, tab) {
|
|
9
|
+
return getComponent(name, tab);
|
|
10
|
+
}
|
|
11
|
+
listComponents(includeDrafts = false) {
|
|
12
|
+
return listComponentMeta(includeDrafts);
|
|
13
|
+
}
|
|
14
|
+
getFoundation(name) {
|
|
15
|
+
return getFoundation(name);
|
|
16
|
+
}
|
|
17
|
+
listFoundations() {
|
|
18
|
+
return listFoundations();
|
|
19
|
+
}
|
|
20
|
+
searchDocs(query, limit = 5) {
|
|
21
|
+
return searchDocs(query, limit);
|
|
22
|
+
}
|
|
23
|
+
listExercises(includeDrafts = false) {
|
|
24
|
+
return listExerciseMeta(includeDrafts);
|
|
25
|
+
}
|
|
26
|
+
getExerciseDetail(name) {
|
|
27
|
+
return getExerciseDetail(name);
|
|
28
|
+
}
|
|
29
|
+
getExerciseStep(name, step) {
|
|
30
|
+
return getExerciseStep(name, step);
|
|
31
|
+
}
|
|
32
|
+
listRecipes() {
|
|
33
|
+
return listRecipeMeta();
|
|
34
|
+
}
|
|
35
|
+
getRecipe(name) {
|
|
36
|
+
return getRecipe(name);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Factory — selects provider based on environment / filesystem presence.
|
|
6
|
+
//
|
|
7
|
+
// Selection order:
|
|
8
|
+
// 1. RUNSNATIVE_CONTENT_ROOT env var set → LocalContentProvider
|
|
9
|
+
// 2. ../../../content exists relative to dist/ → LocalContentProvider
|
|
10
|
+
// 3. Otherwise → RemoteContentProvider
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export async function createProvider() {
|
|
13
|
+
if (process.env['RUNSNATIVE_CONTENT_ROOT']) {
|
|
14
|
+
const { LocalContentProvider } = await import('./local-provider.js');
|
|
15
|
+
return new LocalContentProvider();
|
|
16
|
+
}
|
|
17
|
+
// Probe for the local content tree relative to this module's location.
|
|
18
|
+
// In the compiled dist/ layout: dist/provider.js → ../../../content
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const localContent = join(__dirname, '../../../content');
|
|
21
|
+
if (existsSync(localContent)) {
|
|
22
|
+
const { LocalContentProvider } = await import('./local-provider.js');
|
|
23
|
+
return new LocalContentProvider();
|
|
24
|
+
}
|
|
25
|
+
const { RemoteContentProvider } = await import('./remote-provider.js');
|
|
26
|
+
return new RemoteContentProvider();
|
|
27
|
+
}
|