lumo-mobile 0.0.1__tar.gz

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.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: lumo-mobile
3
+ Version: 0.0.1
4
+ Summary: Deterministic mobile UI/UX checks for Compose / SwiftUI / XML / UIKit — WCAG validator with OKLCH auto-correct, cross-platform parity diff, cognitive-science (Fitts / Hick / Gestalt) layout checks. Ships with an MCP server.
5
+ Author: Viktor Savchik
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/OneXeor/lumo
8
+ Project-URL: Repository, https://github.com/OneXeor/lumo
9
+ Project-URL: Issues, https://github.com/OneXeor/lumo/issues
10
+ Keywords: mobile,design,ui,ux,wcag,accessibility,jetpack-compose,swiftui,uikit,android,ios,fitts,gestalt,design-system,cross-platform,mcp
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Software Development :: User Interfaces
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: numpy>=1.26
25
+ Requires-Dist: mcp>=1.27
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.4; extra == "dev"
29
+ Requires-Dist: mypy>=1.10; extra == "dev"
30
+
31
+ # lumo-mobile
32
+
33
+ Deterministic mobile UI/UX checks invoked by [Lumo](https://github.com/OneXeor/lumo) — a
34
+ Claude Code skill / MCP server / CLI toolkit for designing polished mobile
35
+ apps on Jetpack Compose, Android XML, SwiftUI, and UIKit.
36
+
37
+ Install:
38
+
39
+ ```bash
40
+ pipx install lumo-mobile
41
+ ```
42
+
43
+ Three CLIs (plus one MCP server) ship:
44
+
45
+ | Command | What it does |
46
+ |---|---|
47
+ | `lumo-wcag check --fg <hex> --bg <hex>` | WCAG AA / AAA contrast verdict using the W3C luminance formula. |
48
+ | `lumo-wcag fix --fg <hex> --bg <hex>` | OKLCH auto-correct that preserves hue and chroma while pushing the contrast above the threshold. |
49
+ | `lumo-theory check --layout <path>` | Cognitive-science layout checks: Fitts (undersized targets, relative difficulty for primaries), Hick overload, Gestalt proximity, one-handed reachability. |
50
+ | `lumo-parity diff --android <path> --ios <path> [--config <path>]` | Cross-platform diff between Android (dp) and iOS (pt) layouts, with optional design-system token validation. |
51
+ | `lumo-mcp` | Model Context Protocol server (stdio) exposing all of the above to Claude Code, Cursor, Continue, Aider, Goose, Zed, Codex. |
52
+
53
+ See the [main repo](https://github.com/OneXeor/lumo) for the full SKILL.md,
54
+ examples, and rationale.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,28 @@
1
+ # lumo-mobile
2
+
3
+ Deterministic mobile UI/UX checks invoked by [Lumo](https://github.com/OneXeor/lumo) — a
4
+ Claude Code skill / MCP server / CLI toolkit for designing polished mobile
5
+ apps on Jetpack Compose, Android XML, SwiftUI, and UIKit.
6
+
7
+ Install:
8
+
9
+ ```bash
10
+ pipx install lumo-mobile
11
+ ```
12
+
13
+ Three CLIs (plus one MCP server) ship:
14
+
15
+ | Command | What it does |
16
+ |---|---|
17
+ | `lumo-wcag check --fg <hex> --bg <hex>` | WCAG AA / AAA contrast verdict using the W3C luminance formula. |
18
+ | `lumo-wcag fix --fg <hex> --bg <hex>` | OKLCH auto-correct that preserves hue and chroma while pushing the contrast above the threshold. |
19
+ | `lumo-theory check --layout <path>` | Cognitive-science layout checks: Fitts (undersized targets, relative difficulty for primaries), Hick overload, Gestalt proximity, one-handed reachability. |
20
+ | `lumo-parity diff --android <path> --ios <path> [--config <path>]` | Cross-platform diff between Android (dp) and iOS (pt) layouts, with optional design-system token validation. |
21
+ | `lumo-mcp` | Model Context Protocol server (stdio) exposing all of the above to Claude Code, Cursor, Continue, Aider, Goose, Zed, Codex. |
22
+
23
+ See the [main repo](https://github.com/OneXeor/lumo) for the full SKILL.md,
24
+ examples, and rationale.
25
+
26
+ ## License
27
+
28
+ MIT
@@ -0,0 +1,3 @@
1
+ """Lumo — mobile design tools invoked by the Lumo Claude Code skill."""
2
+
3
+ __version__ = "0.0.1"
@@ -0,0 +1,10 @@
1
+ """MCP server exposing Lumo tools to any MCP-compatible client.
2
+
3
+ Public API:
4
+ server — the FastMCP instance (importable for tests and custom mounts)
5
+ main() — stdio entrypoint, registered as `lumo-mcp` console script
6
+ """
7
+
8
+ from lumo.mcp.server import main, server
9
+
10
+ __all__ = ["main", "server"]
@@ -0,0 +1,249 @@
1
+ """Lumo MCP server.
2
+
3
+ Exposes the three Lumo tools (WCAG, theory, parity) over the Model Context
4
+ Protocol so any MCP-compatible client (Claude Code, Cursor, Continue,
5
+ Aider, Goose, Zed, etc.) can call them with structured arguments.
6
+
7
+ This is a thin wrapper over the existing Python API — the heavy lifting
8
+ stays in lumo.wcag, lumo.theory, lumo.parity. Adding MCP did not change a
9
+ single line of those modules.
10
+
11
+ Transport: stdio (the MCP standard for local servers).
12
+
13
+ Run directly:
14
+ lumo-mcp
15
+
16
+ Or for development:
17
+ python -m lumo.mcp.server
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import asdict
23
+ from typing import Any, Literal
24
+
25
+ from mcp.server.fastmcp import FastMCP
26
+
27
+ from lumo.parity.core import DesignSystemConfig, diff
28
+ from lumo.theory.core import Element, Layout, Screen, check_layout
29
+ from lumo.wcag.core import auto_correct, check_pair
30
+
31
+ server = FastMCP("lumo")
32
+
33
+
34
+ # ============================================================================
35
+ # Tool 1 — WCAG check
36
+ # ============================================================================
37
+
38
+
39
+ @server.tool()
40
+ def lumo_wcag_check(
41
+ fg: str,
42
+ bg: str,
43
+ level: Literal["AA", "AAA"] = "AA",
44
+ size: Literal["normal", "large"] = "normal",
45
+ ) -> dict[str, Any]:
46
+ """Check whether a foreground/background color pair meets WCAG contrast.
47
+
48
+ Uses the W3C relative luminance formula. Returns the exact contrast ratio,
49
+ the required threshold for the given level + text size, and a pass / fail
50
+ verdict. This is deterministic math, not an LLM guess.
51
+
52
+ Args:
53
+ fg: Foreground hex (#RGB, #RRGGBB, #RGBA, or #RRGGBBAA — alpha ignored)
54
+ bg: Background hex (same formats)
55
+ level: "AA" (4.5:1 normal / 3:1 large) or "AAA" (7:1 / 4.5:1)
56
+ size: "normal" or "large" (large = ≥18pt or ≥14pt bold)
57
+
58
+ Returns:
59
+ Dict with fg, bg, ratio, level, size, required, passes.
60
+ """
61
+ result = check_pair(fg, bg, level, size)
62
+ return asdict(result)
63
+
64
+
65
+ # ============================================================================
66
+ # Tool 2 — WCAG auto-correct
67
+ # ============================================================================
68
+
69
+
70
+ @server.tool()
71
+ def lumo_wcag_fix(
72
+ fg: str,
73
+ bg: str,
74
+ level: Literal["AA", "AAA"] = "AA",
75
+ size: Literal["normal", "large"] = "normal",
76
+ max_iterations: int = 60,
77
+ ) -> dict[str, Any]:
78
+ """Auto-correct a failing foreground color to meet WCAG, preserving hue and chroma.
79
+
80
+ Adjusts the foreground's L-channel in OKLCH (perceptually uniform color
81
+ space) until the pair passes. Brand identity stays intact because chroma
82
+ and hue are held fixed. Returns the corrected hex plus iteration count
83
+ and direction (darken_fg / lighten_fg / unchanged).
84
+
85
+ Args:
86
+ fg: Foreground hex to correct
87
+ bg: Background hex (untouched)
88
+ level: "AA" or "AAA"
89
+ size: "normal" or "large"
90
+ max_iterations: safety bound on the iterative search
91
+
92
+ Returns:
93
+ Dict with original CheckResult, corrected_fg, corrected_bg, corrected
94
+ CheckResult, iterations, and strategy.
95
+ """
96
+ result = auto_correct(fg, bg, level, size, max_iterations)
97
+ return {
98
+ "original": asdict(result.original),
99
+ "corrected_fg": result.corrected_fg,
100
+ "corrected_bg": result.corrected_bg,
101
+ "corrected": asdict(result.corrected),
102
+ "iterations": result.iterations,
103
+ "strategy": result.strategy,
104
+ }
105
+
106
+
107
+ # ============================================================================
108
+ # Tool 3 — theory_check
109
+ # ============================================================================
110
+
111
+
112
+ @server.tool()
113
+ def lumo_theory_check(layout: dict[str, Any]) -> dict[str, Any]:
114
+ """Run cognitive-science layout checks (Fitts, Hick, Gestalt, reach).
115
+
116
+ Accepts a layout JSON (same schema as the lumo-theory CLI). Returns
117
+ findings with severity, recommendation, and the metric that produced
118
+ them. Each finding inherits a confidence label from the layout's
119
+ `source` field — `measured` / `code-estimated` / `description-estimated`
120
+ — so the consumer can weigh trust honestly.
121
+
122
+ Tool does NOT produce absolute Fitts MT or Hick RT in ms. Those depend
123
+ on device-specific constants with ±40% variance; we return relative
124
+ ratios and discrete flags instead.
125
+
126
+ Args:
127
+ layout: Layout JSON with keys `screen` ({width, height, unit}),
128
+ `source` (one of measured | code-estimated |
129
+ description-estimated), and `elements` (list of element
130
+ dicts with id, role, x, y, w, h, optional group + weight).
131
+
132
+ Returns:
133
+ Dict with `source`, `counts_by_severity`, and `findings` list.
134
+ """
135
+ screen = Screen(
136
+ width=float(layout["screen"]["width"]),
137
+ height=float(layout["screen"]["height"]),
138
+ unit=layout["screen"].get("unit", "dp"),
139
+ )
140
+ elements = tuple(
141
+ Element(
142
+ id=str(e["id"]),
143
+ role=e["role"],
144
+ x=float(e["x"]),
145
+ y=float(e["y"]),
146
+ w=float(e["w"]),
147
+ h=float(e["h"]),
148
+ group=e.get("group"),
149
+ weight=e.get("weight", "equal"),
150
+ )
151
+ for e in layout.get("elements", [])
152
+ )
153
+ parsed = Layout(screen=screen, elements=elements, source=layout.get("source", "description-estimated"))
154
+ report = check_layout(parsed)
155
+ return {
156
+ "source": report.source,
157
+ "counts_by_severity": report.counts_by_severity,
158
+ "findings": [asdict(f) for f in report.findings],
159
+ }
160
+
161
+
162
+ # ============================================================================
163
+ # Tool 4 — platform_parity
164
+ # ============================================================================
165
+
166
+
167
+ @server.tool()
168
+ def lumo_parity_diff(
169
+ android: dict[str, Any],
170
+ ios: dict[str, Any],
171
+ config: dict[str, Any] | None = None,
172
+ ) -> dict[str, Any]:
173
+ """Diff an Android (dp) and iOS (pt) layout, optionally against a design system.
174
+
175
+ Compares the two layouts for component presence and sizing mismatches.
176
+ Whitelists known platform-specific defaults (Material 48dp vs Apple HIG
177
+ 44pt touch target; Material bottom nav 80dp vs iOS Tab Bar 49pt) and
178
+ reports them as `info`, not as mismatches.
179
+
180
+ Reminder: dp and pt are both density-independent and equal in physical
181
+ size on screen. 16dp matches 16pt. The classic "iOS uses 3× because
182
+ Retina" misconception (writing 48pt on iOS for 16dp Android) is exactly
183
+ the bug this tool catches.
184
+
185
+ Args:
186
+ android: Android layout JSON (same schema as lumo_theory_check)
187
+ ios: iOS layout JSON
188
+ config: Optional design-system config with `spacing`, `sizing`,
189
+ `colors` token maps
190
+
191
+ Returns:
192
+ Dict with `confidence`, `android_source`, `ios_source`,
193
+ `counts_by_severity`, and `findings` list.
194
+ """
195
+
196
+ def _to_layout(data: dict[str, Any]) -> Layout:
197
+ screen = Screen(
198
+ width=float(data["screen"]["width"]),
199
+ height=float(data["screen"]["height"]),
200
+ unit=data["screen"].get("unit", "dp"),
201
+ )
202
+ elements = tuple(
203
+ Element(
204
+ id=str(e["id"]),
205
+ role=e["role"],
206
+ x=float(e["x"]),
207
+ y=float(e["y"]),
208
+ w=float(e["w"]),
209
+ h=float(e["h"]),
210
+ group=e.get("group"),
211
+ weight=e.get("weight", "equal"),
212
+ )
213
+ for e in data.get("elements", [])
214
+ )
215
+ return Layout(screen=screen, elements=elements, source=data.get("source", "description-estimated"))
216
+
217
+ android_layout = _to_layout(android)
218
+ ios_layout = _to_layout(ios)
219
+
220
+ ds_config = None
221
+ if config is not None:
222
+ ds_config = DesignSystemConfig(
223
+ spacing=config.get("spacing", {}),
224
+ sizing=config.get("sizing", {}),
225
+ colors=config.get("colors", {}),
226
+ )
227
+
228
+ report = diff(android_layout, ios_layout, ds_config)
229
+ return {
230
+ "confidence": report.confidence,
231
+ "android_source": report.android_source,
232
+ "ios_source": report.ios_source,
233
+ "counts_by_severity": report.counts_by_severity,
234
+ "findings": [asdict(f) for f in report.findings],
235
+ }
236
+
237
+
238
+ # ============================================================================
239
+ # Entrypoint
240
+ # ============================================================================
241
+
242
+
243
+ def main() -> None:
244
+ """Run the Lumo MCP server over stdio (the MCP standard for local tools)."""
245
+ server.run(transport="stdio")
246
+
247
+
248
+ if __name__ == "__main__":
249
+ main()
@@ -0,0 +1,31 @@
1
+ """Cross-platform parity diff for Android (Compose / XML) vs iOS (SwiftUI / UIKit).
2
+
3
+ Public API:
4
+ diff(android, ios, config=None) -> ParityReport
5
+
6
+ The diff compares two layout JSONs (same schema as theory_check) and, when
7
+ provided, validates both against a shared design-system config.
8
+
9
+ Honest design notes:
10
+ - dp and pt are both density-independent. A 16dp / 16pt comparison is
11
+ legitimate (NOT 16dp / 48pt — that's a known junior misconception).
12
+ - Some numeric discrepancies are *expected* per platform standards
13
+ (44pt touch target vs 48dp, 49pt Tab Bar vs 80dp bottom nav,
14
+ 17pt iOS body text vs 16sp Material). These live in the whitelist
15
+ and are reported as `info`, not `mismatch`.
16
+ - Confidence propagates from the lower of the two input sources.
17
+ """
18
+
19
+ from lumo.parity.core import (
20
+ DesignSystemConfig,
21
+ ParityFinding,
22
+ ParityReport,
23
+ diff,
24
+ )
25
+
26
+ __all__ = [
27
+ "DesignSystemConfig",
28
+ "ParityFinding",
29
+ "ParityReport",
30
+ "diff",
31
+ ]
@@ -0,0 +1,123 @@
1
+ """CLI for platform_parity.
2
+
3
+ Usage:
4
+ lumo-parity diff --android <path|-> --ios <path|-> [--config <path>] [--json]
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from dataclasses import asdict
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from lumo.parity.core import DesignSystemConfig, diff
17
+ from lumo.theory.core import Element, Layout, Screen
18
+
19
+
20
+ def _load_layout(source: str) -> Layout:
21
+ raw = sys.stdin.read() if source == "-" else Path(source).read_text(encoding="utf-8")
22
+ data: dict[str, Any] = json.loads(raw)
23
+ screen = Screen(
24
+ width=float(data["screen"]["width"]),
25
+ height=float(data["screen"]["height"]),
26
+ unit=data["screen"].get("unit", "dp"),
27
+ )
28
+ elements = tuple(
29
+ Element(
30
+ id=str(e["id"]),
31
+ role=e["role"],
32
+ x=float(e["x"]),
33
+ y=float(e["y"]),
34
+ w=float(e["w"]),
35
+ h=float(e["h"]),
36
+ group=e.get("group"),
37
+ weight=e.get("weight", "equal"),
38
+ )
39
+ for e in data.get("elements", [])
40
+ )
41
+ return Layout(screen=screen, elements=elements, source=data.get("source", "description-estimated"))
42
+
43
+
44
+ def _load_config(path: str) -> DesignSystemConfig:
45
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
46
+ return DesignSystemConfig(
47
+ spacing=data.get("spacing", {}),
48
+ sizing=data.get("sizing", {}),
49
+ colors=data.get("colors", {}),
50
+ )
51
+
52
+
53
+ def _print_human(report: Any) -> None:
54
+ if not report.findings:
55
+ print("OK Android and iOS layouts are in parity.")
56
+ print(f" confidence: {report.confidence}")
57
+ print(f" android: {report.android_source}, ios: {report.ios_source}")
58
+ return
59
+
60
+ counts = report.counts_by_severity
61
+ severity_summary = ", ".join(
62
+ f"{n} {sev}" for sev, n in sorted(counts.items(), key=lambda x: x[0])
63
+ )
64
+ print(f"FOUND {len(report.findings)} parity findings ({severity_summary})")
65
+ print(f" confidence: {report.confidence}")
66
+ print(f" android: {report.android_source}, ios: {report.ios_source}\n")
67
+
68
+ for i, f in enumerate(report.findings, 1):
69
+ print(f" {i}. [{f.severity.upper():8}] {f.check}")
70
+ if f.element_id:
71
+ print(f" element: {f.element_id}")
72
+ if f.android_value is not None or f.ios_value is not None:
73
+ print(f" android: {f.android_value} ios: {f.ios_value}")
74
+ print(f" {f.message}")
75
+ print(f" → {f.recommendation}")
76
+ print()
77
+
78
+
79
+ def _print_json(report: Any) -> None:
80
+ payload = {
81
+ "confidence": report.confidence,
82
+ "android_source": report.android_source,
83
+ "ios_source": report.ios_source,
84
+ "counts_by_severity": report.counts_by_severity,
85
+ "findings": [asdict(f) for f in report.findings],
86
+ }
87
+ print(json.dumps(payload, indent=2, default=str))
88
+
89
+
90
+ def main(argv: list[str] | None = None) -> int:
91
+ parser = argparse.ArgumentParser(
92
+ prog="lumo-parity",
93
+ description="Cross-platform parity diff: Android (dp) vs iOS (pt).",
94
+ )
95
+ sub = parser.add_subparsers(dest="cmd", required=True)
96
+
97
+ d = sub.add_parser("diff", help="Diff two layouts and (optionally) validate against a design system.")
98
+ d.add_argument("--android", required=True, help="Path to Android layout JSON, or '-' for stdin.")
99
+ d.add_argument("--ios", required=True, help="Path to iOS layout JSON, or '-' for stdin.")
100
+ d.add_argument("--config", default=None, help="Optional lumo.config.json with design tokens.")
101
+ d.add_argument("--json", action="store_true", help="Emit JSON.")
102
+
103
+ args = parser.parse_args(argv)
104
+
105
+ if args.cmd == "diff":
106
+ if args.android == "-" and args.ios == "-":
107
+ print("error: cannot read both layouts from stdin", file=sys.stderr)
108
+ return 2
109
+ android = _load_layout(args.android)
110
+ ios = _load_layout(args.ios)
111
+ config = _load_config(args.config) if args.config else None
112
+ report = diff(android, ios, config)
113
+ if args.json:
114
+ _print_json(report)
115
+ else:
116
+ _print_human(report)
117
+ return 0 if not report.findings else 1
118
+
119
+ return 2
120
+
121
+
122
+ if __name__ == "__main__":
123
+ sys.exit(main())