@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 CHANGED
@@ -1828,6 +1828,245 @@ function createDesignHandlers(registry) {
1828
1828
  };
1829
1829
  }
1830
1830
 
1831
+ // src/server/page-health.ts
1832
+ var GRID_SIZE = 20;
1833
+ var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
1834
+ var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
1835
+ var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
1836
+ "button",
1837
+ "input",
1838
+ "switch",
1839
+ "checkbox",
1840
+ "radio",
1841
+ "touchable",
1842
+ "listItem"
1843
+ ]);
1844
+ var ERROR_PHRASES = [
1845
+ "error occurred",
1846
+ "failed to",
1847
+ "exception",
1848
+ "crash",
1849
+ "unavailable",
1850
+ "something went wrong",
1851
+ "could not"
1852
+ ];
1853
+ var LOADING_PHRASES = [
1854
+ "loading",
1855
+ "starting",
1856
+ "connecting",
1857
+ "please wait",
1858
+ "initializing",
1859
+ "fetching"
1860
+ ];
1861
+ var EMPTY_PHRASES = [
1862
+ "no data",
1863
+ "no results",
1864
+ "nothing here",
1865
+ "empty",
1866
+ "no items",
1867
+ "get started"
1868
+ ];
1869
+ function severityRank(s) {
1870
+ if (s === "CRITICAL") return 3;
1871
+ if (s === "WARNING") return 2;
1872
+ return 1;
1873
+ }
1874
+ function worstSeverity(findings) {
1875
+ let worst = "OK";
1876
+ for (const f of findings) {
1877
+ if (severityRank(f.severity) > severityRank(worst)) {
1878
+ worst = f.severity;
1879
+ }
1880
+ }
1881
+ return worst;
1882
+ }
1883
+ function truncate(s, n) {
1884
+ if (s.length <= n) return s;
1885
+ return [...s].slice(0, n).join("");
1886
+ }
1887
+ function normalizeRect(state, viewport) {
1888
+ const layout = state.layout;
1889
+ if (!layout) return null;
1890
+ if (viewport.width <= 0 || viewport.height <= 0) return null;
1891
+ const px = layout.pageX ?? layout.x;
1892
+ const py = layout.pageY ?? layout.y;
1893
+ return {
1894
+ x: px / viewport.width,
1895
+ y: py / viewport.height,
1896
+ width: layout.width / viewport.width,
1897
+ height: layout.height / viewport.height
1898
+ };
1899
+ }
1900
+ function diagnosePageHealth(elements, viewport) {
1901
+ const findings = [];
1902
+ const projected = elements.map((el) => ({
1903
+ el,
1904
+ rect: el.state.visible ? normalizeRect(el.state, viewport) : null
1905
+ }));
1906
+ const visible = projected.filter((p) => p.rect !== null);
1907
+ const grid = Array.from(
1908
+ { length: GRID_SIZE },
1909
+ () => Array.from({ length: GRID_SIZE }, () => false)
1910
+ );
1911
+ for (const { rect } of visible) {
1912
+ const colStart = Math.max(0, Math.floor(rect.x * GRID_SIZE));
1913
+ const colEnd = Math.min(GRID_SIZE, Math.ceil((rect.x + rect.width) * GRID_SIZE));
1914
+ const rowStart = Math.max(0, Math.floor(rect.y * GRID_SIZE));
1915
+ const rowEnd = Math.min(GRID_SIZE, Math.ceil((rect.y + rect.height) * GRID_SIZE));
1916
+ for (let r = rowStart; r < rowEnd; r++) {
1917
+ for (let c = colStart; c < colEnd; c++) {
1918
+ grid[r][c] = true;
1919
+ }
1920
+ }
1921
+ }
1922
+ const totalCells = GRID_SIZE * GRID_SIZE;
1923
+ const halfCells = totalCells / 2;
1924
+ let filled = 0;
1925
+ let leftFilled = 0;
1926
+ let rightFilled = 0;
1927
+ const half = GRID_SIZE / 2;
1928
+ for (let r = 0; r < GRID_SIZE; r++) {
1929
+ for (let c = 0; c < GRID_SIZE; c++) {
1930
+ if (grid[r][c]) {
1931
+ filled++;
1932
+ if (c < half) leftFilled++;
1933
+ else rightFilled++;
1934
+ }
1935
+ }
1936
+ }
1937
+ const coveragePct = Math.round(filled / totalCells * 100);
1938
+ const leftHalfPct = Math.round(leftFilled / halfCells * 100);
1939
+ const rightHalfPct = Math.round(rightFilled / halfCells * 100);
1940
+ let spatialSeverity = "OK";
1941
+ if (coveragePct < 15) spatialSeverity = "CRITICAL";
1942
+ else if (coveragePct < 30) spatialSeverity = "WARNING";
1943
+ if (rightHalfPct < 5 && leftHalfPct > 20) spatialSeverity = "CRITICAL";
1944
+ findings.push({
1945
+ check: "spatial_coverage",
1946
+ severity: spatialSeverity,
1947
+ detail: `Elements cover ${coveragePct}% of viewport. Left=${leftHalfPct}%, Right=${rightHalfPct}%`,
1948
+ data: {
1949
+ coverage_pct: coveragePct,
1950
+ left_half_pct: leftHalfPct,
1951
+ right_half_pct: rightHalfPct
1952
+ }
1953
+ });
1954
+ let sidebar = 0;
1955
+ let header = 0;
1956
+ let content = 0;
1957
+ for (const { rect } of visible) {
1958
+ const cx = rect.x + rect.width / 2;
1959
+ const cy = rect.y + rect.height / 2;
1960
+ if (cx < 0.2) sidebar++;
1961
+ else if (cy < 0.08) header++;
1962
+ else content++;
1963
+ }
1964
+ let layoutSeverity = "OK";
1965
+ if (content === 0) layoutSeverity = "CRITICAL";
1966
+ else if (content < 3) layoutSeverity = "WARNING";
1967
+ findings.push({
1968
+ check: "layout_regions",
1969
+ severity: layoutSeverity,
1970
+ detail: `sidebar=${sidebar}, header=${header}, content=${content}`,
1971
+ data: { sidebar, header, content }
1972
+ });
1973
+ const typeCounts = {};
1974
+ for (const el of elements) {
1975
+ const t = el.type || "unknown";
1976
+ typeCounts[t] = (typeCounts[t] ?? 0) + 1;
1977
+ }
1978
+ const allNav = elements.length > 5 && Object.keys(typeCounts).every((k) => NAV_TYPES.has(k));
1979
+ const diversitySeverity = allNav ? "WARNING" : "OK";
1980
+ findings.push({
1981
+ check: "element_diversity",
1982
+ severity: diversitySeverity,
1983
+ detail: `${Object.keys(typeCounts).length} type(s) across ${elements.length} elements${allNav ? " (navigation-only)" : ""}`,
1984
+ data: { types: typeCounts }
1985
+ });
1986
+ const detectedErrors = [];
1987
+ const detectedLoading = [];
1988
+ const detectedEmpty = [];
1989
+ const detectedCss = [];
1990
+ for (const el of elements) {
1991
+ const elType = el.type ?? "";
1992
+ if (SKIP_TEXT_TYPES.has(elType)) continue;
1993
+ const text = (el.state.textContent ?? "").toString();
1994
+ if (!text) continue;
1995
+ const textLower = text.toLowerCase();
1996
+ for (const p of ERROR_PHRASES) {
1997
+ if (textLower.includes(p)) {
1998
+ detectedErrors.push(truncate(text, 120));
1999
+ break;
2000
+ }
2001
+ }
2002
+ for (const p of LOADING_PHRASES) {
2003
+ if (textLower.includes(p)) {
2004
+ detectedLoading.push(truncate(text, 120));
2005
+ break;
2006
+ }
2007
+ }
2008
+ for (const p of EMPTY_PHRASES) {
2009
+ if (textLower.includes(p)) {
2010
+ detectedEmpty.push(truncate(text, 120));
2011
+ break;
2012
+ }
2013
+ }
2014
+ }
2015
+ let textSeverity = "OK";
2016
+ if (detectedErrors.length > 0) textSeverity = "CRITICAL";
2017
+ else if (detectedLoading.length > 0 || detectedEmpty.length > 0) textSeverity = "WARNING";
2018
+ findings.push({
2019
+ check: "text_signals",
2020
+ severity: textSeverity,
2021
+ detail: `errors=${detectedErrors.length}, loading=${detectedLoading.length}, empty=${detectedEmpty.length}, css_signals=${detectedCss.length}`,
2022
+ data: {
2023
+ errors: detectedErrors,
2024
+ loading: detectedLoading,
2025
+ empty: detectedEmpty,
2026
+ css_signals: detectedCss
2027
+ }
2028
+ });
2029
+ let interactiveTotal = 0;
2030
+ let interactiveDisabled = 0;
2031
+ for (const el of elements) {
2032
+ if (!INTERACTIVE_TYPES.has(el.type)) continue;
2033
+ interactiveTotal++;
2034
+ if (el.state.enabled === false) interactiveDisabled++;
2035
+ }
2036
+ const interactiveSeverity = interactiveTotal > 0 && interactiveDisabled / interactiveTotal > 0.5 ? "WARNING" : "OK";
2037
+ findings.push({
2038
+ check: "interactive_readiness",
2039
+ severity: interactiveSeverity,
2040
+ detail: `${interactiveTotal} interactive elements, ${interactiveDisabled} disabled`,
2041
+ data: { total: interactiveTotal, disabled: interactiveDisabled }
2042
+ });
2043
+ let zeroSize = 0;
2044
+ let outsideViewport = 0;
2045
+ for (const { rect } of visible) {
2046
+ if (rect.width === 0 || rect.height === 0) zeroSize++;
2047
+ if (rect.x + rect.width < 0 || rect.y + rect.height < 0 || rect.x > 1 || rect.y > 1) {
2048
+ outsideViewport++;
2049
+ }
2050
+ }
2051
+ const anomalySeverity = zeroSize > 0 || outsideViewport > 0 ? "WARNING" : "OK";
2052
+ findings.push({
2053
+ check: "visual_anomalies",
2054
+ severity: anomalySeverity,
2055
+ detail: `zero_size=${zeroSize}, outside_viewport=${outsideViewport}`,
2056
+ data: { zero_size: zeroSize, outside_viewport: outsideViewport }
2057
+ });
2058
+ const heatmap = grid.map(
2059
+ (row) => row.map((cell) => cell ? "#" : ".").join("")
2060
+ );
2061
+ return {
2062
+ summary: worstSeverity(findings),
2063
+ findings,
2064
+ heatmap,
2065
+ element_count: elements.length,
2066
+ visible_count: visible.length
2067
+ };
2068
+ }
2069
+
1831
2070
  // src/server/handlers.ts
1832
2071
  async function executeWorkflowStep(step, registry, executor) {
1833
2072
  const startTime = Date.now();
@@ -2007,6 +2246,15 @@ async function executeWorkflowStep(step, registry, executor) {
2007
2246
  };
2008
2247
  }
2009
2248
  }
2249
+ function getWindowDimensions() {
2250
+ try {
2251
+ const { Dimensions: Dimensions2 } = __require("react-native");
2252
+ const win = Dimensions2.get("window");
2253
+ return { width: win.width, height: win.height };
2254
+ } catch {
2255
+ return { width: 0, height: 0 };
2256
+ }
2257
+ }
2010
2258
  function success2(data) {
2011
2259
  return {
2012
2260
  success: true,
@@ -2212,6 +2460,16 @@ function createServerHandlers(registry, executor, config) {
2212
2460
  const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
2213
2461
  return success2(snapshot);
2214
2462
  },
2463
+ getPageHealth: async (ctx) => {
2464
+ const bodyViewport = ctx.body?.viewport;
2465
+ const viewport = bodyViewport ?? getWindowDimensions();
2466
+ const elements = registry.getAllElements().map((e) => ({
2467
+ type: e.type,
2468
+ state: e.getState()
2469
+ }));
2470
+ const report = diagnosePageHealth(elements, viewport);
2471
+ return success2(report);
2472
+ },
2215
2473
  // Workflows
2216
2474
  getWorkflows: async () => {
2217
2475
  const workflows = registry.getAllWorkflows().map((w) => ({
@@ -2960,6 +3218,10 @@ var WS_ROUTES = [
2960
3218
  { pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
2961
3219
  { pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
2962
3220
  { pattern: "control/discover", handler: "getSnapshot" },
3221
+ // Page health — accepts both GET and POST so the canonical skill invocation
3222
+ // (`POST .../control/page-health` with an empty body) and ad-hoc curls both
3223
+ // work without a body-shape gotcha.
3224
+ { pattern: "control/page-health", handler: "getPageHealth", httpMethods: ["GET", "POST"] },
2963
3225
  // Workflows
2964
3226
  { pattern: "control/workflows", handler: "getWorkflows", httpMethods: ["GET"] },
2965
3227
  { pattern: "control/workflow/:id/run", handler: "runWorkflow", httpMethods: ["POST"] },
@@ -3064,6 +3326,7 @@ var HANDLER_DESCRIPTIONS = {
3064
3326
  executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
3065
3327
  find: "AI-powered element search by natural-language query",
3066
3328
  getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
3329
+ 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").',
3067
3330
  getWorkflows: "List registered workflows",
3068
3331
  runWorkflow: "Trigger a workflow by id",
3069
3332
  pageRefresh: "Reload the current route",
@@ -5797,6 +6060,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
5797
6060
  path: "/ui-bridge/control/snapshot",
5798
6061
  description: "Get full bridge snapshot"
5799
6062
  },
6063
+ PAGE_HEALTH: {
6064
+ method: "POST",
6065
+ path: "/ui-bridge/control/page-health",
6066
+ description: "Holistic page health diagnostic: spatial coverage, layout regions, text signals, interactive readiness, anomalies + ASCII heatmap"
6067
+ },
5800
6068
  // Workflows
5801
6069
  GET_WORKFLOWS: {
5802
6070
  method: "GET",