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