@qontinui/ui-bridge-native 0.6.2 → 0.6.4
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/dist/index.js +268 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +268 -0
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +263 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +263 -0
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +268 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +268 -0
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/page-health.d.ts +72 -0
- package/dist/server/page-health.d.ts.map +1 -0
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/react/index.mjs
CHANGED
|
@@ -1774,6 +1774,245 @@ function createDesignHandlers(registry) {
|
|
|
1774
1774
|
};
|
|
1775
1775
|
}
|
|
1776
1776
|
|
|
1777
|
+
// src/server/page-health.ts
|
|
1778
|
+
var GRID_SIZE = 20;
|
|
1779
|
+
var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
|
|
1780
|
+
var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
|
|
1781
|
+
var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
1782
|
+
"button",
|
|
1783
|
+
"input",
|
|
1784
|
+
"switch",
|
|
1785
|
+
"checkbox",
|
|
1786
|
+
"radio",
|
|
1787
|
+
"touchable",
|
|
1788
|
+
"listItem"
|
|
1789
|
+
]);
|
|
1790
|
+
var ERROR_PHRASES = [
|
|
1791
|
+
"error occurred",
|
|
1792
|
+
"failed to",
|
|
1793
|
+
"exception",
|
|
1794
|
+
"crash",
|
|
1795
|
+
"unavailable",
|
|
1796
|
+
"something went wrong",
|
|
1797
|
+
"could not"
|
|
1798
|
+
];
|
|
1799
|
+
var LOADING_PHRASES = [
|
|
1800
|
+
"loading",
|
|
1801
|
+
"starting",
|
|
1802
|
+
"connecting",
|
|
1803
|
+
"please wait",
|
|
1804
|
+
"initializing",
|
|
1805
|
+
"fetching"
|
|
1806
|
+
];
|
|
1807
|
+
var EMPTY_PHRASES = [
|
|
1808
|
+
"no data",
|
|
1809
|
+
"no results",
|
|
1810
|
+
"nothing here",
|
|
1811
|
+
"empty",
|
|
1812
|
+
"no items",
|
|
1813
|
+
"get started"
|
|
1814
|
+
];
|
|
1815
|
+
function severityRank(s) {
|
|
1816
|
+
if (s === "CRITICAL") return 3;
|
|
1817
|
+
if (s === "WARNING") return 2;
|
|
1818
|
+
return 1;
|
|
1819
|
+
}
|
|
1820
|
+
function worstSeverity(findings) {
|
|
1821
|
+
let worst = "OK";
|
|
1822
|
+
for (const f of findings) {
|
|
1823
|
+
if (severityRank(f.severity) > severityRank(worst)) {
|
|
1824
|
+
worst = f.severity;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return worst;
|
|
1828
|
+
}
|
|
1829
|
+
function truncate(s, n) {
|
|
1830
|
+
if (s.length <= n) return s;
|
|
1831
|
+
return [...s].slice(0, n).join("");
|
|
1832
|
+
}
|
|
1833
|
+
function normalizeRect(state, viewport) {
|
|
1834
|
+
const layout = state.layout;
|
|
1835
|
+
if (!layout) return null;
|
|
1836
|
+
if (viewport.width <= 0 || viewport.height <= 0) return null;
|
|
1837
|
+
const px = layout.pageX ?? layout.x;
|
|
1838
|
+
const py = layout.pageY ?? layout.y;
|
|
1839
|
+
return {
|
|
1840
|
+
x: px / viewport.width,
|
|
1841
|
+
y: py / viewport.height,
|
|
1842
|
+
width: layout.width / viewport.width,
|
|
1843
|
+
height: layout.height / viewport.height
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
function diagnosePageHealth(elements, viewport) {
|
|
1847
|
+
const findings = [];
|
|
1848
|
+
const projected = elements.map((el) => ({
|
|
1849
|
+
el,
|
|
1850
|
+
rect: el.state.visible ? normalizeRect(el.state, viewport) : null
|
|
1851
|
+
}));
|
|
1852
|
+
const visible = projected.filter((p) => p.rect !== null);
|
|
1853
|
+
const grid = Array.from(
|
|
1854
|
+
{ length: GRID_SIZE },
|
|
1855
|
+
() => Array.from({ length: GRID_SIZE }, () => false)
|
|
1856
|
+
);
|
|
1857
|
+
for (const { rect } of visible) {
|
|
1858
|
+
const colStart = Math.max(0, Math.floor(rect.x * GRID_SIZE));
|
|
1859
|
+
const colEnd = Math.min(GRID_SIZE, Math.ceil((rect.x + rect.width) * GRID_SIZE));
|
|
1860
|
+
const rowStart = Math.max(0, Math.floor(rect.y * GRID_SIZE));
|
|
1861
|
+
const rowEnd = Math.min(GRID_SIZE, Math.ceil((rect.y + rect.height) * GRID_SIZE));
|
|
1862
|
+
for (let r = rowStart; r < rowEnd; r++) {
|
|
1863
|
+
for (let c = colStart; c < colEnd; c++) {
|
|
1864
|
+
grid[r][c] = true;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const totalCells = GRID_SIZE * GRID_SIZE;
|
|
1869
|
+
const halfCells = totalCells / 2;
|
|
1870
|
+
let filled = 0;
|
|
1871
|
+
let leftFilled = 0;
|
|
1872
|
+
let rightFilled = 0;
|
|
1873
|
+
const half = GRID_SIZE / 2;
|
|
1874
|
+
for (let r = 0; r < GRID_SIZE; r++) {
|
|
1875
|
+
for (let c = 0; c < GRID_SIZE; c++) {
|
|
1876
|
+
if (grid[r][c]) {
|
|
1877
|
+
filled++;
|
|
1878
|
+
if (c < half) leftFilled++;
|
|
1879
|
+
else rightFilled++;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
const coveragePct = Math.round(filled / totalCells * 100);
|
|
1884
|
+
const leftHalfPct = Math.round(leftFilled / halfCells * 100);
|
|
1885
|
+
const rightHalfPct = Math.round(rightFilled / halfCells * 100);
|
|
1886
|
+
let spatialSeverity = "OK";
|
|
1887
|
+
if (coveragePct < 15) spatialSeverity = "CRITICAL";
|
|
1888
|
+
else if (coveragePct < 30) spatialSeverity = "WARNING";
|
|
1889
|
+
if (rightHalfPct < 5 && leftHalfPct > 20) spatialSeverity = "CRITICAL";
|
|
1890
|
+
findings.push({
|
|
1891
|
+
check: "spatial_coverage",
|
|
1892
|
+
severity: spatialSeverity,
|
|
1893
|
+
detail: `Elements cover ${coveragePct}% of viewport. Left=${leftHalfPct}%, Right=${rightHalfPct}%`,
|
|
1894
|
+
data: {
|
|
1895
|
+
coverage_pct: coveragePct,
|
|
1896
|
+
left_half_pct: leftHalfPct,
|
|
1897
|
+
right_half_pct: rightHalfPct
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
let sidebar = 0;
|
|
1901
|
+
let header = 0;
|
|
1902
|
+
let content = 0;
|
|
1903
|
+
for (const { rect } of visible) {
|
|
1904
|
+
const cx = rect.x + rect.width / 2;
|
|
1905
|
+
const cy = rect.y + rect.height / 2;
|
|
1906
|
+
if (cx < 0.2) sidebar++;
|
|
1907
|
+
else if (cy < 0.08) header++;
|
|
1908
|
+
else content++;
|
|
1909
|
+
}
|
|
1910
|
+
let layoutSeverity = "OK";
|
|
1911
|
+
if (content === 0) layoutSeverity = "CRITICAL";
|
|
1912
|
+
else if (content < 3) layoutSeverity = "WARNING";
|
|
1913
|
+
findings.push({
|
|
1914
|
+
check: "layout_regions",
|
|
1915
|
+
severity: layoutSeverity,
|
|
1916
|
+
detail: `sidebar=${sidebar}, header=${header}, content=${content}`,
|
|
1917
|
+
data: { sidebar, header, content }
|
|
1918
|
+
});
|
|
1919
|
+
const typeCounts = {};
|
|
1920
|
+
for (const el of elements) {
|
|
1921
|
+
const t = el.type || "unknown";
|
|
1922
|
+
typeCounts[t] = (typeCounts[t] ?? 0) + 1;
|
|
1923
|
+
}
|
|
1924
|
+
const allNav = elements.length > 5 && Object.keys(typeCounts).every((k) => NAV_TYPES.has(k));
|
|
1925
|
+
const diversitySeverity = allNav ? "WARNING" : "OK";
|
|
1926
|
+
findings.push({
|
|
1927
|
+
check: "element_diversity",
|
|
1928
|
+
severity: diversitySeverity,
|
|
1929
|
+
detail: `${Object.keys(typeCounts).length} type(s) across ${elements.length} elements${allNav ? " (navigation-only)" : ""}`,
|
|
1930
|
+
data: { types: typeCounts }
|
|
1931
|
+
});
|
|
1932
|
+
const detectedErrors = [];
|
|
1933
|
+
const detectedLoading = [];
|
|
1934
|
+
const detectedEmpty = [];
|
|
1935
|
+
const detectedCss = [];
|
|
1936
|
+
for (const el of elements) {
|
|
1937
|
+
const elType = el.type ?? "";
|
|
1938
|
+
if (SKIP_TEXT_TYPES.has(elType)) continue;
|
|
1939
|
+
const text = (el.state.textContent ?? "").toString();
|
|
1940
|
+
if (!text) continue;
|
|
1941
|
+
const textLower = text.toLowerCase();
|
|
1942
|
+
for (const p of ERROR_PHRASES) {
|
|
1943
|
+
if (textLower.includes(p)) {
|
|
1944
|
+
detectedErrors.push(truncate(text, 120));
|
|
1945
|
+
break;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
for (const p of LOADING_PHRASES) {
|
|
1949
|
+
if (textLower.includes(p)) {
|
|
1950
|
+
detectedLoading.push(truncate(text, 120));
|
|
1951
|
+
break;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
for (const p of EMPTY_PHRASES) {
|
|
1955
|
+
if (textLower.includes(p)) {
|
|
1956
|
+
detectedEmpty.push(truncate(text, 120));
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
let textSeverity = "OK";
|
|
1962
|
+
if (detectedErrors.length > 0) textSeverity = "CRITICAL";
|
|
1963
|
+
else if (detectedLoading.length > 0 || detectedEmpty.length > 0) textSeverity = "WARNING";
|
|
1964
|
+
findings.push({
|
|
1965
|
+
check: "text_signals",
|
|
1966
|
+
severity: textSeverity,
|
|
1967
|
+
detail: `errors=${detectedErrors.length}, loading=${detectedLoading.length}, empty=${detectedEmpty.length}, css_signals=${detectedCss.length}`,
|
|
1968
|
+
data: {
|
|
1969
|
+
errors: detectedErrors,
|
|
1970
|
+
loading: detectedLoading,
|
|
1971
|
+
empty: detectedEmpty,
|
|
1972
|
+
css_signals: detectedCss
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
let interactiveTotal = 0;
|
|
1976
|
+
let interactiveDisabled = 0;
|
|
1977
|
+
for (const el of elements) {
|
|
1978
|
+
if (!INTERACTIVE_TYPES.has(el.type)) continue;
|
|
1979
|
+
interactiveTotal++;
|
|
1980
|
+
if (el.state.enabled === false) interactiveDisabled++;
|
|
1981
|
+
}
|
|
1982
|
+
const interactiveSeverity = interactiveTotal > 0 && interactiveDisabled / interactiveTotal > 0.5 ? "WARNING" : "OK";
|
|
1983
|
+
findings.push({
|
|
1984
|
+
check: "interactive_readiness",
|
|
1985
|
+
severity: interactiveSeverity,
|
|
1986
|
+
detail: `${interactiveTotal} interactive elements, ${interactiveDisabled} disabled`,
|
|
1987
|
+
data: { total: interactiveTotal, disabled: interactiveDisabled }
|
|
1988
|
+
});
|
|
1989
|
+
let zeroSize = 0;
|
|
1990
|
+
let outsideViewport = 0;
|
|
1991
|
+
for (const { rect } of visible) {
|
|
1992
|
+
if (rect.width === 0 || rect.height === 0) zeroSize++;
|
|
1993
|
+
if (rect.x + rect.width < 0 || rect.y + rect.height < 0 || rect.x > 1 || rect.y > 1) {
|
|
1994
|
+
outsideViewport++;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const anomalySeverity = zeroSize > 0 || outsideViewport > 0 ? "WARNING" : "OK";
|
|
1998
|
+
findings.push({
|
|
1999
|
+
check: "visual_anomalies",
|
|
2000
|
+
severity: anomalySeverity,
|
|
2001
|
+
detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport}`,
|
|
2002
|
+
data: { zero_size: zeroSize, outside_viewport: outsideViewport }
|
|
2003
|
+
});
|
|
2004
|
+
const heatmap = grid.map(
|
|
2005
|
+
(row) => row.map((cell) => cell ? "#" : ".").join("")
|
|
2006
|
+
);
|
|
2007
|
+
return {
|
|
2008
|
+
summary: worstSeverity(findings),
|
|
2009
|
+
findings,
|
|
2010
|
+
heatmap,
|
|
2011
|
+
element_count: elements.length,
|
|
2012
|
+
visible_count: visible.length
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1777
2016
|
// src/server/handlers.ts
|
|
1778
2017
|
async function executeWorkflowStep(step, registry, executor) {
|
|
1779
2018
|
const startTime = Date.now();
|
|
@@ -1953,6 +2192,15 @@ async function executeWorkflowStep(step, registry, executor) {
|
|
|
1953
2192
|
};
|
|
1954
2193
|
}
|
|
1955
2194
|
}
|
|
2195
|
+
function getWindowDimensions() {
|
|
2196
|
+
try {
|
|
2197
|
+
const { Dimensions } = __require("react-native");
|
|
2198
|
+
const win = Dimensions.get("window");
|
|
2199
|
+
return { width: win.width, height: win.height };
|
|
2200
|
+
} catch {
|
|
2201
|
+
return { width: 0, height: 0 };
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
1956
2204
|
function success2(data) {
|
|
1957
2205
|
return {
|
|
1958
2206
|
success: true,
|
|
@@ -2158,6 +2406,16 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2158
2406
|
const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
|
|
2159
2407
|
return success2(snapshot);
|
|
2160
2408
|
},
|
|
2409
|
+
getPageHealth: async (ctx) => {
|
|
2410
|
+
const bodyViewport = ctx.body?.viewport;
|
|
2411
|
+
const viewport = bodyViewport ?? getWindowDimensions();
|
|
2412
|
+
const elements = registry.getAllElements().map((e) => ({
|
|
2413
|
+
type: e.type,
|
|
2414
|
+
state: e.getState()
|
|
2415
|
+
}));
|
|
2416
|
+
const report = diagnosePageHealth(elements, viewport);
|
|
2417
|
+
return success2(report);
|
|
2418
|
+
},
|
|
2161
2419
|
// Workflows
|
|
2162
2420
|
getWorkflows: async () => {
|
|
2163
2421
|
const workflows = registry.getAllWorkflows().map((w) => ({
|
|
@@ -2906,6 +3164,10 @@ var WS_ROUTES = [
|
|
|
2906
3164
|
{ pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
|
|
2907
3165
|
{ pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
|
|
2908
3166
|
{ pattern: "control/discover", handler: "getSnapshot" },
|
|
3167
|
+
// Page health — accepts both GET and POST so the canonical skill invocation
|
|
3168
|
+
// (`POST .../control/page-health` with an empty body) and ad-hoc curls both
|
|
3169
|
+
// work without a body-shape gotcha.
|
|
3170
|
+
{ pattern: "control/page-health", handler: "getPageHealth", httpMethods: ["GET", "POST"] },
|
|
2909
3171
|
// Workflows
|
|
2910
3172
|
{ pattern: "control/workflows", handler: "getWorkflows", httpMethods: ["GET"] },
|
|
2911
3173
|
{ pattern: "control/workflow/:id/run", handler: "runWorkflow", httpMethods: ["POST"] },
|
|
@@ -3010,6 +3272,7 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
3010
3272
|
executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
|
|
3011
3273
|
find: "AI-powered element search by natural-language query",
|
|
3012
3274
|
getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
|
|
3275
|
+
getPageHealth: 'Holistic page health diagnostic over current snapshot elements (spatial coverage, layout regions, text/anomaly signals, heatmap). Optional body: { viewport?: { width, height } } to override Dimensions.get("window").',
|
|
3013
3276
|
getWorkflows: "List registered workflows",
|
|
3014
3277
|
runWorkflow: "Trigger a workflow by id",
|
|
3015
3278
|
pageRefresh: "Reload the current route",
|