ourui 0.2.0__tar.gz → 0.2.2__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.
Files changed (38) hide show
  1. {ourui-0.2.0 → ourui-0.2.2}/PKG-INFO +2 -2
  2. {ourui-0.2.0 → ourui-0.2.2}/README.md +1 -1
  3. ourui-0.2.2/ourui/design/__init__.py +5 -0
  4. ourui-0.2.2/ourui/design/resolve.py +121 -0
  5. {ourui-0.2.0 → ourui-0.2.2}/ourui/emit/html.py +72 -5
  6. {ourui-0.2.0 → ourui-0.2.2}/ourui/pipeline.py +13 -2
  7. {ourui-0.2.0 → ourui-0.2.2}/ourui.egg-info/PKG-INFO +2 -2
  8. {ourui-0.2.0 → ourui-0.2.2}/ourui.egg-info/SOURCES.txt +2 -0
  9. {ourui-0.2.0 → ourui-0.2.2}/pyproject.toml +1 -1
  10. {ourui-0.2.0 → ourui-0.2.2}/LICENSE +0 -0
  11. {ourui-0.2.0 → ourui-0.2.2}/ourui/__init__.py +0 -0
  12. {ourui-0.2.0 → ourui-0.2.2}/ourui/analysis/__init__.py +0 -0
  13. {ourui-0.2.0 → ourui-0.2.2}/ourui/analysis/components.py +0 -0
  14. {ourui-0.2.0 → ourui-0.2.2}/ourui/cli.py +0 -0
  15. {ourui-0.2.0 → ourui-0.2.2}/ourui/emit/__init__.py +0 -0
  16. {ourui-0.2.0 → ourui-0.2.2}/ourui/emit/js.py +0 -0
  17. {ourui-0.2.0 → ourui-0.2.2}/ourui/lowering/__init__.py +0 -0
  18. {ourui-0.2.0 → ourui-0.2.2}/ourui/lowering/intent.py +0 -0
  19. {ourui-0.2.0 → ourui-0.2.2}/ourui/lowering/layout.py +0 -0
  20. {ourui-0.2.0 → ourui-0.2.2}/ourui/lowering/presentation.py +0 -0
  21. {ourui-0.2.0 → ourui-0.2.2}/ourui/lowering/render.py +0 -0
  22. {ourui-0.2.0 → ourui-0.2.2}/ourui/lsp/__init__.py +0 -0
  23. {ourui-0.2.0 → ourui-0.2.2}/ourui/lsp/completions.py +0 -0
  24. {ourui-0.2.0 → ourui-0.2.2}/ourui/lsp/protocol.py +0 -0
  25. {ourui-0.2.0 → ourui-0.2.2}/ourui/lsp/server.py +0 -0
  26. {ourui-0.2.0 → ourui-0.2.2}/ourui/node.py +0 -0
  27. {ourui-0.2.0 → ourui-0.2.2}/ourui/parse/__init__.py +0 -0
  28. {ourui-0.2.0 → ourui-0.2.2}/ourui/runtime/__init__.py +0 -0
  29. {ourui-0.2.0 → ourui-0.2.2}/ourui/runtime/hmr.py +0 -0
  30. {ourui-0.2.0 → ourui-0.2.2}/ourui/runtime/invoke.py +0 -0
  31. {ourui-0.2.0 → ourui-0.2.2}/ourui/runtime/session.py +0 -0
  32. {ourui-0.2.0 → ourui-0.2.2}/ourui/serialize/__init__.py +0 -0
  33. {ourui-0.2.0 → ourui-0.2.2}/ourui/theme.py +0 -0
  34. {ourui-0.2.0 → ourui-0.2.2}/ourui/ui.py +0 -0
  35. {ourui-0.2.0 → ourui-0.2.2}/ourui.egg-info/dependency_links.txt +0 -0
  36. {ourui-0.2.0 → ourui-0.2.2}/ourui.egg-info/entry_points.txt +0 -0
  37. {ourui-0.2.0 → ourui-0.2.2}/ourui.egg-info/top_level.txt +0 -0
  38. {ourui-0.2.0 → ourui-0.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ourui
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python-first UI compiler — write intent in Python, emit HTML/CSS/JS
5
5
  Author: OurUI contributors
6
6
  License: MIT License
@@ -46,7 +46,7 @@ Description-Content-Type: text/markdown
46
46
  License-File: LICENSE
47
47
  Dynamic: license-file
48
48
 
49
- # ourui 0.2.0
49
+ # ourui 0.2.2
50
50
 
51
51
  Python package for the **OurUI** compiler and runtime.
52
52
 
@@ -1,4 +1,4 @@
1
- # ourui 0.2.0
1
+ # ourui 0.2.2
2
2
 
3
3
  Python package for the **OurUI** compiler and runtime.
4
4
 
@@ -0,0 +1,5 @@
1
+ # Design resolution package (RFC-002).
2
+
3
+ from ourui.design.resolve import ResolvedDesign, default_pack, resolve_design
4
+
5
+ __all__ = ["ResolvedDesign", "default_pack", "resolve_design"]
@@ -0,0 +1,121 @@
1
+ """Design System resolution: Presentation Graph + pack → Resolved Design (RFC-002)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from ourui.theme import DEFAULT_DARK, DEFAULT_LIGHT
9
+
10
+ PACK_ID = "ourui-default"
11
+
12
+
13
+ def default_pack() -> dict[str, Any]:
14
+ """Seed Design System pack from current theme tables (provisional migration)."""
15
+ return {
16
+ "id": PACK_ID,
17
+ "modes": {
18
+ "light": dict(DEFAULT_LIGHT),
19
+ "dark": dict(DEFAULT_DARK),
20
+ },
21
+ "control": {
22
+ "pad_block": "space_sm",
23
+ "pad_inline": "space_md",
24
+ "radius": "radius",
25
+ },
26
+ }
27
+
28
+
29
+ @dataclass
30
+ class ResolvedDesign:
31
+ pack: str
32
+ mode: str
33
+ nodes: dict[str, dict[str, Any]] = field(default_factory=dict)
34
+ tokens: dict[str, dict[str, str]] = field(default_factory=dict)
35
+
36
+ def to_dict(self) -> dict[str, Any]:
37
+ return {
38
+ "pack": self.pack,
39
+ "mode": self.mode,
40
+ "nodes": {nid: self.nodes[nid] for nid in sorted(self.nodes)},
41
+ "tokens": {
42
+ "light": dict(self.tokens.get("light", {})),
43
+ "dark": dict(self.tokens.get("dark", {})),
44
+ },
45
+ }
46
+
47
+
48
+ def _tone_pair(tone: str | None, mode_tokens: dict[str, str]) -> dict[str, str]:
49
+ if not tone:
50
+ return {
51
+ "fill": mode_tokens.get("muted", ""),
52
+ "fg": mode_tokens.get("muted_fg", ""),
53
+ }
54
+ fill_key = tone
55
+ fg_key = f"{tone}_fg"
56
+ if fill_key not in mode_tokens:
57
+ fill_key = "primary"
58
+ fg_key = "primary_fg"
59
+ return {
60
+ "fill": mode_tokens.get(fill_key, mode_tokens.get("primary", "")),
61
+ "fg": mode_tokens.get(fg_key, mode_tokens.get("primary_fg", "")),
62
+ }
63
+
64
+
65
+ def resolve_design(
66
+ presentation_graph: Any,
67
+ *,
68
+ pack: dict[str, Any] | None = None,
69
+ mode: str = "light",
70
+ token_overrides: dict[str, dict[str, str]] | None = None,
71
+ ) -> ResolvedDesign:
72
+ """Pure resolution: PG + Design System pack → Resolved Design (no CSS)."""
73
+ pack = pack or default_pack()
74
+ modes = {
75
+ "light": dict(pack.get("modes", {}).get("light", DEFAULT_LIGHT)),
76
+ "dark": dict(pack.get("modes", {}).get("dark", DEFAULT_DARK)),
77
+ }
78
+ if token_overrides:
79
+ for m in ("light", "dark"):
80
+ if m in token_overrides:
81
+ modes[m].update(token_overrides[m])
82
+
83
+ mode_key = mode if mode in modes else "light"
84
+ mode_tokens = modes[mode_key]
85
+ control = pack.get("control") or {}
86
+ pad_block = mode_tokens.get(control.get("pad_block", "space_sm"), mode_tokens.get("space_sm", ""))
87
+ pad_inline = mode_tokens.get(control.get("pad_inline", "space_md"), mode_tokens.get("space_md", ""))
88
+ radius = mode_tokens.get(control.get("radius", "radius"), mode_tokens.get("radius", ""))
89
+
90
+ pg = presentation_graph.to_dict() if hasattr(presentation_graph, "to_dict") else dict(presentation_graph)
91
+ nodes_in = pg.get("nodes") or {}
92
+
93
+ out = ResolvedDesign(pack=str(pack.get("id", PACK_ID)), mode=mode_key, tokens=modes)
94
+ for nid, node in nodes_in.items():
95
+ tone = node.get("tone")
96
+ if isinstance(tone, str):
97
+ pair = _tone_pair(tone, mode_tokens)
98
+ else:
99
+ pair = _tone_pair(None, mode_tokens)
100
+ resolved: dict[str, Any] = {
101
+ "fill": pair["fill"],
102
+ "fg": pair["fg"],
103
+ "radius": radius,
104
+ "pad_block": pad_block,
105
+ "pad_inline": pad_inline,
106
+ }
107
+ entry = {
108
+ "id": nid,
109
+ "kind": node.get("kind"),
110
+ "role": node.get("role"),
111
+ "resolved": resolved,
112
+ "provenance": [*node.get("provenance", []), "resolve:design"],
113
+ }
114
+ if tone is not None:
115
+ entry["tone"] = tone
116
+ if "href" in node:
117
+ entry["href"] = node["href"]
118
+ if "shell_layout" in node:
119
+ entry["shell_layout"] = node["shell_layout"]
120
+ out.nodes[nid] = entry
121
+ return out
@@ -98,6 +98,60 @@ button.ourui-tone-muted {
98
98
  color: var(--ourui-muted-fg);
99
99
  }
100
100
  """
101
+ # Tone classes above use CSS vars seeded from Resolved Design (Host Contract).
102
+ # Per-node rules from Resolved Design override for concrete fill/fg/radius/pad.
103
+
104
+
105
+ def _as_resolved_design(resolved_design: Any) -> dict[str, Any] | None:
106
+ if resolved_design is None:
107
+ return None
108
+ if hasattr(resolved_design, "to_dict"):
109
+ return resolved_design.to_dict()
110
+ return dict(resolved_design)
111
+
112
+
113
+ def _tokens_from_resolved(rd: dict[str, Any]) -> dict[str, dict[str, str]]:
114
+ tokens = rd.get("tokens") or {}
115
+ return {
116
+ "light": dict(tokens.get("light") or {}),
117
+ "dark": dict(tokens.get("dark") or {}),
118
+ }
119
+
120
+
121
+ def _emit_resolved_node_css(rd: dict[str, Any]) -> str:
122
+ """Host maps Resolved Design node values → CSS (no Design System tables)."""
123
+ lines: list[str] = []
124
+ nodes = rd.get("nodes") or {}
125
+ for nid in sorted(nodes):
126
+ node = nodes[nid]
127
+ role = node.get("role")
128
+ resolved = node.get("resolved") or {}
129
+ if role not in {"button", "link"}:
130
+ continue
131
+ fill = resolved.get("fill")
132
+ fg = resolved.get("fg")
133
+ if not fill and not fg:
134
+ continue
135
+ lines.append(f'[data-ourui-id="{nid}"] {{')
136
+ if role == "button":
137
+ if fill:
138
+ lines.append(f" background: {fill};")
139
+ if fg:
140
+ lines.append(f" color: {fg};")
141
+ if resolved.get("radius"):
142
+ lines.append(f" border-radius: {resolved['radius']};")
143
+ pad_b = resolved.get("pad_block")
144
+ pad_i = resolved.get("pad_inline")
145
+ if pad_b and pad_i:
146
+ lines.append(f" padding: {pad_b} {pad_i};")
147
+ lines.append(" border-color: transparent;")
148
+ else:
149
+ if fill:
150
+ lines.append(f" color: {fill};")
151
+ lines.append("}")
152
+ if not lines:
153
+ return ""
154
+ return "\n".join(lines) + "\n"
101
155
 
102
156
 
103
157
  def _tone_name(attrs: dict[str, Any]) -> str | None:
@@ -232,7 +286,16 @@ def apply_state_values(rtr: dict[str, Any], state_values: dict[str, Any] | None)
232
286
  return out
233
287
 
234
288
 
235
- def emit_css(tokens: dict[str, dict[str, str]] | None = None) -> str:
289
+ def emit_css(
290
+ tokens: dict[str, dict[str, str]] | None = None,
291
+ *,
292
+ resolved_design: Any = None,
293
+ ) -> str:
294
+ """Emit host CSS. Prefer Resolved Design tokens + per-node rules (Host Contract)."""
295
+ rd = _as_resolved_design(resolved_design)
296
+ if rd is not None:
297
+ token_block = emit_tokens_css(_tokens_from_resolved(rd))
298
+ return token_block + _BASE_CSS + _emit_resolved_node_css(rd)
236
299
  token_block = emit_tokens_css(tokens or default_tokens())
237
300
  return token_block + _BASE_CSS
238
301
 
@@ -244,8 +307,9 @@ def emit_html_document(
244
307
  state_values: dict[str, Any] | None = None,
245
308
  hmr: bool = False,
246
309
  tokens: dict[str, dict[str, str]] | None = None,
310
+ resolved_design: Any = None,
247
311
  ) -> str:
248
- """Emit a full HTML document from an RTR dict (HostNode tree only)."""
312
+ """Emit HTML from Host Contract inputs: RTR + Resolved Design (+ optional tokens fallback)."""
249
313
  rtr = apply_state_values(rtr, state_values)
250
314
  nodes = rtr["nodes"]
251
315
  roots = rtr["roots"]
@@ -255,7 +319,7 @@ def emit_html_document(
255
319
  body_lines.append(" </div>")
256
320
 
257
321
  js = emit_js(rtr, hmr=hmr).rstrip("\n")
258
- css = emit_css(tokens).rstrip("\n")
322
+ css = emit_css(tokens, resolved_design=resolved_design).rstrip("\n")
259
323
  parts = [
260
324
  "<!DOCTYPE html>",
261
325
  '<html lang="en">',
@@ -283,10 +347,13 @@ def emit_bundle(
283
347
  *,
284
348
  title: str = "OurUI",
285
349
  tokens: dict[str, dict[str, str]] | None = None,
350
+ resolved_design: Any = None,
286
351
  ) -> dict[str, str]:
287
352
  """Serializable emit artifacts (I10): html + css + js."""
288
353
  return {
289
- "html": emit_html_document(rtr, title=title, tokens=tokens),
290
- "css": emit_css(tokens),
354
+ "html": emit_html_document(
355
+ rtr, title=title, tokens=tokens, resolved_design=resolved_design
356
+ ),
357
+ "css": emit_css(tokens, resolved_design=resolved_design),
291
358
  "js": emit_js(rtr),
292
359
  }
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
  from typing import Any
5
5
 
6
6
  from ourui.analysis import build_semantic_graph
7
+ from ourui.design import resolve_design
7
8
  from ourui.emit import emit_bundle, emit_html_document
8
9
  from ourui.lowering import lower_to_iir, lower_to_ltr, lower_to_presentation_graph, lower_to_rtr
9
10
  from ourui.serialize import dumps_deterministic
@@ -30,6 +31,10 @@ def compile_to_rtr(path: str | Path, *, route: str | None = None) -> dict[str, A
30
31
  sg.roots = [sg.routes[default_route]]
31
32
  iir = lower_to_iir(sg)
32
33
  presentation_graph = lower_to_presentation_graph(iir)
34
+ resolved_design = resolve_design(
35
+ presentation_graph,
36
+ token_overrides=sg.tokens,
37
+ )
33
38
  ltr = lower_to_ltr(iir)
34
39
  rtr = lower_to_rtr(ltr)
35
40
  return {
@@ -38,6 +43,7 @@ def compile_to_rtr(path: str | Path, *, route: str | None = None) -> dict[str, A
38
43
  "dependency_graph": dg,
39
44
  "iir": iir,
40
45
  "presentation_graph": presentation_graph,
46
+ "resolved_design": resolved_design,
41
47
  "ltr": ltr,
42
48
  "rtr": rtr,
43
49
  }
@@ -47,12 +53,13 @@ def compile_dump(path: str | Path) -> dict[str, Any]:
47
53
  path = Path(path)
48
54
  artifacts = compile_to_rtr(path)
49
55
  return {
50
- "version": 11,
56
+ "version": 12,
51
57
  "source": artifacts["source"],
52
58
  "semantic_graph": artifacts["semantic_graph"].to_dict(),
53
59
  "dependency_graph": artifacts["dependency_graph"].to_dict(),
54
60
  "iir": artifacts["iir"].to_dict(),
55
61
  "presentation_graph": artifacts["presentation_graph"].to_dict(),
62
+ "resolved_design": artifacts["resolved_design"].to_dict(),
56
63
  "ltr": artifacts["ltr"].to_dict(),
57
64
  "rtr": artifacts["rtr"].to_dict(),
58
65
  "emit": {
@@ -63,6 +70,8 @@ def compile_dump(path: str | Path) -> dict[str, Any]:
63
70
  "components": True,
64
71
  "tokens": True,
65
72
  "presentation_graph": True,
73
+ "resolved_design": True,
74
+ "host_contract": True,
66
75
  },
67
76
  }
68
77
 
@@ -79,7 +88,7 @@ def emit_html(
79
88
  state_values: dict[str, Any] | None = None,
80
89
  hmr: bool = False,
81
90
  ) -> str:
82
- """Compile source → RTR → HTML. Emitter consumes HostNode only."""
91
+ """Compile source → RTR + Resolved Design → HTML (Host Contract)."""
83
92
  artifacts = compile_to_rtr(path, route=route)
84
93
  doc_title = title or Path(path).stem
85
94
  return emit_html_document(
@@ -88,6 +97,7 @@ def emit_html(
88
97
  state_values=state_values,
89
98
  hmr=hmr,
90
99
  tokens=artifacts["semantic_graph"].tokens,
100
+ resolved_design=artifacts["resolved_design"],
91
101
  )
92
102
 
93
103
 
@@ -98,4 +108,5 @@ def emit_all(path: str | Path, *, title: str | None = None) -> dict[str, str]:
98
108
  artifacts["rtr"].to_dict(),
99
109
  title=doc_title,
100
110
  tokens=artifacts["semantic_graph"].tokens,
111
+ resolved_design=artifacts["resolved_design"],
101
112
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ourui
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python-first UI compiler — write intent in Python, emit HTML/CSS/JS
5
5
  Author: OurUI contributors
6
6
  License: MIT License
@@ -46,7 +46,7 @@ Description-Content-Type: text/markdown
46
46
  License-File: LICENSE
47
47
  Dynamic: license-file
48
48
 
49
- # ourui 0.2.0
49
+ # ourui 0.2.2
50
50
 
51
51
  Python package for the **OurUI** compiler and runtime.
52
52
 
@@ -14,6 +14,8 @@ ourui.egg-info/entry_points.txt
14
14
  ourui.egg-info/top_level.txt
15
15
  ourui/analysis/__init__.py
16
16
  ourui/analysis/components.py
17
+ ourui/design/__init__.py
18
+ ourui/design/resolve.py
17
19
  ourui/emit/__init__.py
18
20
  ourui/emit/html.py
19
21
  ourui/emit/js.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ourui"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Python-first UI compiler — write intent in Python, emit HTML/CSS/JS"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes