gini-toolkit 6.0.1.dev0__py3-none-any.whl

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 (278) hide show
  1. gini/__init__.py +12 -0
  2. gini/__main__.py +107 -0
  3. gini/_version.py +24 -0
  4. gini/agent/__init__.py +17 -0
  5. gini/agent/agent_gamemaster.py +140 -0
  6. gini/agent/api.py +291 -0
  7. gini/agent/ask.py +123 -0
  8. gini/agent/authoring.py +72 -0
  9. gini/agent/blackboard.py +114 -0
  10. gini/agent/contracts.py +142 -0
  11. gini/agent/domains.py +91 -0
  12. gini/agent/embed.py +123 -0
  13. gini/agent/gamemaster.py +256 -0
  14. gini/agent/kb.py +148 -0
  15. gini/agent/lesson_resolver.py +261 -0
  16. gini/agent/llm/__init__.py +5 -0
  17. gini/agent/llm/backend.py +43 -0
  18. gini/agent/llm/fake.py +25 -0
  19. gini/agent/llm/ollama.py +206 -0
  20. gini/agent/loop.py +258 -0
  21. gini/agent/mcp_server.py +86 -0
  22. gini/agent/meaning.py +225 -0
  23. gini/agent/mission.py +210 -0
  24. gini/agent/mission_controller.py +208 -0
  25. gini/agent/narration.py +116 -0
  26. gini/agent/notifier.py +86 -0
  27. gini/agent/personas.py +79 -0
  28. gini/agent/reasoning.py +172 -0
  29. gini/agent/recall.py +248 -0
  30. gini/agent/session.py +79 -0
  31. gini/agent/teaching_center.py +482 -0
  32. gini/agent/tools/__init__.py +3 -0
  33. gini/agent/tools/registry.py +193 -0
  34. gini/agent/twin/__init__.py +28 -0
  35. gini/agent/twin/authoring.py +71 -0
  36. gini/agent/twin/contracts.py +54 -0
  37. gini/agent/twin/dialectic.py +189 -0
  38. gini/agent/twin/harness.py +93 -0
  39. gini/agent/twin/justify.py +156 -0
  40. gini/agent/twin/learner.py +64 -0
  41. gini/agent/twin/mission.py +60 -0
  42. gini/agent/twin/os_coach.py +79 -0
  43. gini/agent/twin/salience.py +30 -0
  44. gini/agent/understand.py +250 -0
  45. gini/agent/verifiers.py +106 -0
  46. gini/agent/wizard.py +178 -0
  47. gini/agent/xv6_pack.py +74 -0
  48. gini/app/__init__.py +3 -0
  49. gini/app/context.py +368 -0
  50. gini/app/paths.py +121 -0
  51. gini/data/README.md +21 -0
  52. gini/domain/__init__.py +9 -0
  53. gini/domain/assembly.py +209 -0
  54. gini/domain/authoring.py +353 -0
  55. gini/domain/blueprints.py +5 -0
  56. gini/domain/capabilities.py +177 -0
  57. gini/domain/catalog.py +85 -0
  58. gini/domain/certify.py +201 -0
  59. gini/domain/compose.py +413 -0
  60. gini/domain/composition.py +88 -0
  61. gini/domain/concepts.py +383 -0
  62. gini/domain/connection_rules.py +269 -0
  63. gini/domain/constraints.py +153 -0
  64. gini/domain/content.py +59 -0
  65. gini/domain/cpu_journey.py +89 -0
  66. gini/domain/devices.py +747 -0
  67. gini/domain/diagnose.py +201 -0
  68. gini/domain/element_guide.py +327 -0
  69. gini/domain/explain.py +90 -0
  70. gini/domain/fingerprint.py +201 -0
  71. gini/domain/firewall.py +34 -0
  72. gini/domain/flowlog.py +61 -0
  73. gini/domain/flowtable.py +179 -0
  74. gini/domain/fragment_yaml.py +230 -0
  75. gini/domain/fragments.py +169 -0
  76. gini/domain/games/__init__.py +2 -0
  77. gini/domain/games/paging_games.py +119 -0
  78. gini/domain/games/policy_game.py +86 -0
  79. gini/domain/games/process_game.py +48 -0
  80. gini/domain/games/thrash_game.py +75 -0
  81. gini/domain/games/translate_game.py +60 -0
  82. gini/domain/games/trap_game.py +86 -0
  83. gini/domain/grader.py +155 -0
  84. gini/domain/grouping.py +67 -0
  85. gini/domain/legality.py +103 -0
  86. gini/domain/lesson.py +241 -0
  87. gini/domain/lexicon.py +150 -0
  88. gini/domain/machine_state.py +410 -0
  89. gini/domain/missions/networking/basic-lan.yaml +32 -0
  90. gini/domain/missions/networking/cache-in-front.yaml +23 -0
  91. gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
  92. gini/domain/missions/networking/drive-load.yaml +20 -0
  93. gini/domain/missions/networking/fix-the-address.yaml +75 -0
  94. gini/domain/missions/networking/fix-the-lan.yaml +43 -0
  95. gini/domain/missions/networking/inspect-flows.yaml +16 -0
  96. gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
  97. gini/domain/missions/networking/least-privilege.yaml +21 -0
  98. gini/domain/missions/networking/load-balanced-web.yaml +29 -0
  99. gini/domain/missions/networking/observe-it.yaml +24 -0
  100. gini/domain/missions/networking/put-in-vpc.yaml +30 -0
  101. gini/domain/missions/networking/reachability-boundary.yaml +56 -0
  102. gini/domain/missions/networking/sdn-reactive.yaml +35 -0
  103. gini/domain/missions/networking/send-request.yaml +19 -0
  104. gini/domain/missions/networking/serverless-api.yaml +25 -0
  105. gini/domain/missions/networking/service-chain.yaml +33 -0
  106. gini/domain/missions/os/lottery-fix.yaml +19 -0
  107. gini/domain/missions/os/priority-fix.yaml +24 -0
  108. gini/domain/missions.py +111 -0
  109. gini/domain/modulechain.py +36 -0
  110. gini/domain/objectives.py +488 -0
  111. gini/domain/os_zoo.py +79 -0
  112. gini/domain/paging_sim.py +141 -0
  113. gini/domain/pricing.py +199 -0
  114. gini/domain/probes.py +226 -0
  115. gini/domain/profile.py +142 -0
  116. gini/domain/recipes.py +738 -0
  117. gini/domain/riders.py +309 -0
  118. gini/domain/router_modules.py +224 -0
  119. gini/domain/routetable.py +67 -0
  120. gini/domain/scoring.py +76 -0
  121. gini/domain/staging.py +122 -0
  122. gini/domain/syscall_builder.py +144 -0
  123. gini/domain/topic_cloud.py +62 -0
  124. gini/domain/topology.py +213 -0
  125. gini/domain/vocabulary.py +51 -0
  126. gini/domain/xv6.py +808 -0
  127. gini/domain/xv6_fs.py +250 -0
  128. gini/domain/xv6_runner.py +113 -0
  129. gini/domain/xv6_vm.py +385 -0
  130. gini/gloader.py +17 -0
  131. gini/runtime/__init__.py +18 -0
  132. gini/runtime/cloudfabric_agent.py +370 -0
  133. gini/runtime/console.py +68 -0
  134. gini/runtime/control.py +70 -0
  135. gini/runtime/frame.py +138 -0
  136. gini/runtime/gbridge.py +638 -0
  137. gini/runtime/grouter.py +223 -0
  138. gini/runtime/hostsim.py +90 -0
  139. gini/runtime/shuttle.py +348 -0
  140. gini/runtime/switch.py +109 -0
  141. gini/runtime/transport.py +77 -0
  142. gini/runtime/xv6_bridge.py +312 -0
  143. gini/server/__init__.py +22 -0
  144. gini/server/__main__.py +74 -0
  145. gini/server/app.py +140 -0
  146. gini/server/auth.py +82 -0
  147. gini/server/policy.py +57 -0
  148. gini/server/session.py +23 -0
  149. gini/services/__init__.py +15 -0
  150. gini/services/boardflash.py +248 -0
  151. gini/services/boardsetup.py +374 -0
  152. gini/services/cloud_catalog.py +143 -0
  153. gini/services/compiler.py +1858 -0
  154. gini/services/discovery.py +324 -0
  155. gini/services/gloader.py +183 -0
  156. gini/services/orchestrator.py +1460 -0
  157. gini/services/persistence.py +28 -0
  158. gini/services/probe_runner.py +149 -0
  159. gini/services/project.py +217 -0
  160. gini/services/remote.py +93 -0
  161. gini/services/rider_runner.py +96 -0
  162. gini/services/rider_session.py +171 -0
  163. gini/services/shadow_store.py +52 -0
  164. gini/services/terminal.py +45 -0
  165. gini/setup/__init__.py +17 -0
  166. gini/setup/cli.py +109 -0
  167. gini/setup/images.py +33 -0
  168. gini/setup/marker.py +43 -0
  169. gini/setup/runtime.py +69 -0
  170. gini/ui/__init__.py +3 -0
  171. gini/ui/assets/app_icon.icns +0 -0
  172. gini/ui/assets/app_icon.ico +0 -0
  173. gini/ui/assets/app_icon.png +0 -0
  174. gini/ui/assets/app_icon_1024.png +0 -0
  175. gini/ui/assets/cue/_w.txt +1 -0
  176. gini/ui/assets/cue/ai.png +0 -0
  177. gini/ui/assets/cue/canvas.png +0 -0
  178. gini/ui/assets/cue/cloud.png +0 -0
  179. gini/ui/assets/cue/cost.png +0 -0
  180. gini/ui/assets/cue/dark/ai.png +0 -0
  181. gini/ui/assets/cue/dark/canvas.png +0 -0
  182. gini/ui/assets/cue/dark/cloud.png +0 -0
  183. gini/ui/assets/cue/dark/cost.png +0 -0
  184. gini/ui/assets/cue/dark/metrics.png +0 -0
  185. gini/ui/assets/cue/dark/router.png +0 -0
  186. gini/ui/assets/cue/dark/run.png +0 -0
  187. gini/ui/assets/cue/dark/serverless.png +0 -0
  188. gini/ui/assets/cue/dark/settings.png +0 -0
  189. gini/ui/assets/cue/dark/welcome.png +0 -0
  190. gini/ui/assets/cue/dark/wizard.png +0 -0
  191. gini/ui/assets/cue/ginibrand/ai.png +0 -0
  192. gini/ui/assets/cue/ginibrand/canvas.png +0 -0
  193. gini/ui/assets/cue/ginibrand/cloud.png +0 -0
  194. gini/ui/assets/cue/ginibrand/cost.png +0 -0
  195. gini/ui/assets/cue/ginibrand/metrics.png +0 -0
  196. gini/ui/assets/cue/ginibrand/router.png +0 -0
  197. gini/ui/assets/cue/ginibrand/run.png +0 -0
  198. gini/ui/assets/cue/ginibrand/serverless.png +0 -0
  199. gini/ui/assets/cue/ginibrand/settings.png +0 -0
  200. gini/ui/assets/cue/ginibrand/welcome.png +0 -0
  201. gini/ui/assets/cue/ginibrand/wizard.png +0 -0
  202. gini/ui/assets/cue/highcontrast/ai.png +0 -0
  203. gini/ui/assets/cue/highcontrast/canvas.png +0 -0
  204. gini/ui/assets/cue/highcontrast/cloud.png +0 -0
  205. gini/ui/assets/cue/highcontrast/cost.png +0 -0
  206. gini/ui/assets/cue/highcontrast/metrics.png +0 -0
  207. gini/ui/assets/cue/highcontrast/router.png +0 -0
  208. gini/ui/assets/cue/highcontrast/run.png +0 -0
  209. gini/ui/assets/cue/highcontrast/serverless.png +0 -0
  210. gini/ui/assets/cue/highcontrast/settings.png +0 -0
  211. gini/ui/assets/cue/highcontrast/welcome.png +0 -0
  212. gini/ui/assets/cue/highcontrast/wizard.png +0 -0
  213. gini/ui/assets/cue/light/ai.png +0 -0
  214. gini/ui/assets/cue/light/canvas.png +0 -0
  215. gini/ui/assets/cue/light/cloud.png +0 -0
  216. gini/ui/assets/cue/light/cost.png +0 -0
  217. gini/ui/assets/cue/light/metrics.png +0 -0
  218. gini/ui/assets/cue/light/router.png +0 -0
  219. gini/ui/assets/cue/light/run.png +0 -0
  220. gini/ui/assets/cue/light/serverless.png +0 -0
  221. gini/ui/assets/cue/light/settings.png +0 -0
  222. gini/ui/assets/cue/light/welcome.png +0 -0
  223. gini/ui/assets/cue/light/wizard.png +0 -0
  224. gini/ui/assets/cue/metrics.png +0 -0
  225. gini/ui/assets/cue/router.png +0 -0
  226. gini/ui/assets/cue/run.png +0 -0
  227. gini/ui/assets/cue/serverless.png +0 -0
  228. gini/ui/assets/cue/settings.png +0 -0
  229. gini/ui/assets/cue/welcome.png +0 -0
  230. gini/ui/assets/cue/wizard.png +0 -0
  231. gini/ui/assistant.py +2111 -0
  232. gini/ui/author_dialog.py +184 -0
  233. gini/ui/board_dialog.py +247 -0
  234. gini/ui/branding.py +21 -0
  235. gini/ui/canvas.py +2007 -0
  236. gini/ui/chat_panel.py +7 -0
  237. gini/ui/cpu_journey.py +212 -0
  238. gini/ui/cpu_lab.py +306 -0
  239. gini/ui/cue_cards.py +214 -0
  240. gini/ui/dashboard.py +222 -0
  241. gini/ui/diagnose_game.py +336 -0
  242. gini/ui/fingerprint_lab.py +219 -0
  243. gini/ui/flash_dialog.py +244 -0
  244. gini/ui/flow_layout.py +63 -0
  245. gini/ui/fragment_manager.py +1415 -0
  246. gini/ui/game_catalog.py +184 -0
  247. gini/ui/game_renderers.py +340 -0
  248. gini/ui/games_lab.py +90 -0
  249. gini/ui/inspector.py +1055 -0
  250. gini/ui/live_metrics.py +130 -0
  251. gini/ui/machine_lab.py +1412 -0
  252. gini/ui/main_window.py +3153 -0
  253. gini/ui/memory_lab.py +371 -0
  254. gini/ui/mission_panel.py +302 -0
  255. gini/ui/mode_indicator.py +227 -0
  256. gini/ui/palette.py +112 -0
  257. gini/ui/peripherals.py +218 -0
  258. gini/ui/process_tree.py +130 -0
  259. gini/ui/reset_dialog.py +179 -0
  260. gini/ui/router_lab.py +776 -0
  261. gini/ui/run_button.py +183 -0
  262. gini/ui/settings_dialog.py +234 -0
  263. gini/ui/signin_dialog.py +111 -0
  264. gini/ui/storage_lab.py +219 -0
  265. gini/ui/syscall_builder.py +235 -0
  266. gini/ui/syscall_lab.py +152 -0
  267. gini/ui/theme/__init__.py +5 -0
  268. gini/ui/theme/icons.py +145 -0
  269. gini/ui/theme/manager.py +291 -0
  270. gini/ui/theme/tokens.py +194 -0
  271. gini/ui/trap_lab.py +270 -0
  272. gini/ui/worker_host.py +102 -0
  273. gini/ui/zoo_lab.py +112 -0
  274. gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
  275. gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
  276. gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
  277. gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
  278. gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/theme/icons.py ADDED
@@ -0,0 +1,145 @@
1
+ """License-safe flat line-icon set, recolored per theme/category and rendered to QIcon.
2
+
3
+ Icons are hand-authored single-color SVG line glyphs (our own work, no third-party
4
+ licensing) drawn on a 24x24 grid. They take their color from the active theme, so the
5
+ same glyph reads correctly on dark, light, and brand themes. Rendering goes through
6
+ QSvgRenderer at the target device-pixel ratio for crisp output on HiDPI displays.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from PySide6.QtCore import QByteArray, QRectF, Qt
11
+ from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap
12
+
13
+ try:
14
+ from PySide6.QtSvg import QSvgRenderer
15
+ _HAVE_SVG = True
16
+ except Exception: # pragma: no cover
17
+ _HAVE_SVG = False
18
+
19
+
20
+ # inner SVG body for each glyph (stroke-based unless listed in _FILLED)
21
+ _BODY: dict[str, str] = {
22
+ # --- networking ---
23
+ "router": '<rect x="2.5" y="9" width="19" height="9" rx="2"/><path d="M6.5 9V6M12 9V6M17.5 9V6"/><path d="M6.5 13.5h.01M10 13.5h.01M13.5 13.5h.01"/>',
24
+ "switch": '<rect x="2.5" y="8" width="19" height="8" rx="2"/><path d="M6 16v2.5M10 16v2.5M14 16v2.5M18 16v2.5M6 8V5.5M18 8V5.5"/>',
25
+ "hub": '<circle cx="12" cy="12" r="3"/><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"/>',
26
+ "host": '<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/>',
27
+ "firewall": '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9.3h18M3 14.6h18M9 4v5.3M15 9.3v5.3M9 14.6V20"/>',
28
+ "wifi": '<path d="M5 12.5a10 10 0 0 1 14 0"/><path d="M8 15.5a6 6 0 0 1 8 0"/><path d="M11 18.4a1.6 1.6 0 0 1 2 0"/>',
29
+ "cloud": '<path d="M7 18a4 4 0 0 1 0-8 5 5 0 0 1 9.6-1.4A3.5 3.5 0 0 1 18 18z"/>',
30
+ # GINI32: a small board with a radiating antenna — a real chip, not a cloud
31
+ "gini32": '<rect x="3" y="10" width="11" height="9" rx="1.5"/>'
32
+ '<path d="M6 19v2M11 19v2M6 10V8.5M11 10V8.5"/>'
33
+ '<path d="M17 9.5a4 4 0 0 1 0 5M19.5 7a7 7 0 0 1 0 10"/>',
34
+ # --- sdn ---
35
+ "ovs": '<rect x="2.5" y="9" width="19" height="7" rx="2"/><path d="M12 3v6"/><path d="M7 20l5-3 5 3"/>',
36
+ "controller": '<rect x="4" y="4" width="16" height="16" rx="3"/><circle cx="12" cy="11" r="2.5"/><path d="M12 13.5V17M9.5 11H7M17 11h-2.5"/>',
37
+ # --- containers & k8s ---
38
+ "container": '<path d="M3 7.5 12 3l9 4.5v9L12 21l-9-4.5z"/><path d="M3 7.5 12 12l9-4.5M12 12v9"/>',
39
+ "pod": '<path d="M12 2.5l8.2 4.7v9.6L12 21.5 3.8 16.8V7.2z"/><circle cx="12" cy="12" r="3"/>',
40
+ "k8s_node": '<rect x="3" y="6" width="18" height="12" rx="2"/><circle cx="12" cy="12" r="2.5"/><path d="M7 4v2M17 4v2M7 18v2M17 18v2"/>',
41
+ "k8s_cluster": '<path d="M12 2.5l8.2 4.7v9.6L12 21.5 3.8 16.8V7.2z"/><path d="M12 8v8M8.5 10v4M15.5 10v4"/>',
42
+ "registry": '<rect x="3" y="9" width="18" height="11" rx="2"/><path d="M7 9V5h10v4"/><path d="M8 14h8"/>',
43
+ # --- cloud networking ---
44
+ "vpc": '<rect x="2.5" y="4" width="19" height="16" rx="3" stroke-dasharray="4 3"/><path d="M7 12h10M12 8v8"/>',
45
+ "cloud_subnet": '<rect x="3.5" y="6" width="17" height="12" rx="2" stroke-dasharray="3 3"/><path d="M8 12h8"/>',
46
+ "security_group": '<path d="M12 3l7 3v5c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z"/><path d="M9.3 12l1.8 1.8L15 10"/>',
47
+ "gateway": '<path d="M3 12h18"/><path d="M8 7l-4 5 4 5M16 7l4 5-4 5"/>',
48
+ "load_balancer": '<circle cx="12" cy="5" r="2.3"/><circle cx="5" cy="19" r="2.3"/><circle cx="12" cy="19" r="2.3"/><circle cx="19" cy="19" r="2.3"/><path d="M12 7.3v3M12 10.3l-7 6.4M12 10.3v6.4M12 10.3l7 6.4"/>',
49
+ # --- compute ---
50
+ "instance": '<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 3v3M12 3v3M15 3v3M9 18v3M12 18v3M15 18v3M3 9h3M3 12h3M3 15h3M18 9h3M18 12h3M18 15h3"/>',
51
+ "instance_group": '<rect x="7" y="3.5" width="13" height="13" rx="2"/><path d="M4 7.5v13h13"/>',
52
+ "region": '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.6 2.4 4 5.6 4 9s-1.4 6.6-4 9c-2.6-2.4-4-5.6-4-9s1.4-6.6 4-9z"/>',
53
+ # --- storage ---
54
+ "object_store": '<path d="M5 7h14l-1.4 12.2a1 1 0 0 1-1 .9H7.4a1 1 0 0 1-1-.9z"/><path d="M3.5 7h17"/>',
55
+ "block_volume": '<rect x="4" y="6" width="16" height="12" rx="2"/><path d="M8 6v12M4 12h4"/>',
56
+ "database": '<ellipse cx="12" cy="6" rx="7" ry="3"/><path d="M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6"/><path d="M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3"/>',
57
+ # --- serverless ---
58
+ "function": '<path d="M6 20l6-8M18 20 9 4H6"/>',
59
+ "api_gateway": '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M9 9l-2 3 2 3M15 9l2 3-2 3"/>',
60
+ "queue": '<rect x="3" y="7.5" width="4" height="9" rx="1"/><rect x="10" y="7.5" width="4" height="9" rx="1"/><rect x="17" y="7.5" width="4" height="9" rx="1"/>',
61
+ # --- edge & traffic ---
62
+ "proxy": '<path d="M3 12h7M21 12h-7"/><rect x="9" y="8" width="6" height="8" rx="1.5"/><path d="M5 9l-2 3 2 3M19 9l2 3-2 3"/>',
63
+ "web_app": '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 9h18M6 7h.01M8.5 7h.01"/>',
64
+ # --- streaming & messaging ---
65
+ "stream": '<path d="M3 8c3 0 3-2 6-2s3 2 6 2 3-2 6-2M3 12c3 0 3-2 6-2s3 2 6 2 3-2 6-2M3 16c3 0 3-2 6-2s3 2 6 2 3-2 6-2"/>',
66
+ "messaging": '<path d="M4 5h16v11H8l-4 3z"/><path d="M8 9h8M8 12h5"/>',
67
+ # --- cache & nosql ---
68
+ "cache": '<rect x="3.5" y="5" width="17" height="14" rx="2"/><path d="M11 8l-2.5 4H12l-2 4"/>',
69
+ "nosql": '<path d="M12 3c4 4 4 14 0 18-4-4-4-14 0-18z"/><path d="M5.5 8.5c4 2 9 2 13 0M5.5 15.5c4-2 9-2 13 0"/>',
70
+ # --- observability ---
71
+ "metrics": '<path d="M3 20V4M3 20h18"/><path d="M7 16v-3M11.5 16V9M16 16v-5M20.5 16V7"/>',
72
+ "dashboard": '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 15l3-4 3 2 4-6"/>',
73
+ "tracing": '<circle cx="5" cy="7" r="2"/><circle cx="12" cy="17" r="2"/><circle cx="19" cy="9" r="2"/><path d="M6.6 8.4l4 7M13.6 15.8l4-5.4"/>',
74
+ # --- workload & testing ---
75
+ "load_generator": '<path d="M12 13a8 8 0 0 1 8-8M12 13l5-5"/><path d="M4 19a8 8 0 0 1 2.3-12.7M20 19a8 8 0 0 0-2.3-12.7" stroke-dasharray="2 2.5"/>',
76
+ # --- UI glyphs ---
77
+ "new": '<path d="M12 5v14M5 12h14"/>',
78
+ "open": '<path d="M3 7h6l2 2h10v9a2 2 0 0 1-2 2H3z"/>',
79
+ "save": '<path d="M5 3h11l3 3v15H5z"/><path d="M8 3v5h7M8 13h8v8H8z"/>',
80
+ "compile": '<path d="M9 6l-5 6 5 6M15 6l5 6-5 6"/>',
81
+ "layout": '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
82
+ "search": '<circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/>',
83
+ "plus": '<path d="M12 5v14M5 12h14"/>',
84
+ "minus": '<path d="M5 12h14"/>',
85
+ "palette": '<circle cx="12" cy="12" r="9"/><circle cx="8.5" cy="9.5" r="1.1"/><circle cx="12" cy="8" r="1.1"/><circle cx="15.5" cy="9.5" r="1.1"/>',
86
+ "robot": '<rect x="4" y="8" width="16" height="11" rx="3"/><path d="M12 8V4M9 13h.01M15 13h.01M9.5 16h5M2 12v3M22 12v3"/>',
87
+ "send": '<path d="M4 12l16-7-7 16-2-7z"/>',
88
+ "grid": '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/>',
89
+ "trash": '<path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>',
90
+ "link": '<path d="M9 15l6-6M8.5 8H6a4 4 0 0 0 0 8h2.5M15.5 16H18a4 4 0 0 0 0-8h-2.5"/>',
91
+ "elbow": '<path d="M5 5v6a3 3 0 0 0 3 3h6a3 3 0 0 1 3 3v2"/><circle cx="5" cy="5" r="1.9"/><circle cx="20" cy="19" r="1.9"/>',
92
+ "pencil": '<path d="M4 20.5h4L19 9.5a2 2 0 0 0 0-2.8l-1.7-1.7a2 2 0 0 0-2.8 0L3.5 16.5z"/><path d="M13.5 6.5l4 4"/>',
93
+ "chevron_right": '<path d="M9 6l6 6-6 6"/>',
94
+ "chevron_down": '<path d="M6 9l6 6 6-6"/>',
95
+ }
96
+
97
+ _FILLED = {"play", "stop", "dot"}
98
+ _BODY["play"] = '<path d="M8 5v14l11-7z"/>'
99
+ _BODY["stop"] = '<rect x="6" y="6" width="12" height="12" rx="2"/>'
100
+ _BODY["dot"] = '<circle cx="12" cy="12" r="6"/>'
101
+
102
+
103
+ def _svg(name: str, color: str, stroke_width: float) -> str:
104
+ body = _BODY.get(name, _BODY["host"])
105
+ if name in _FILLED:
106
+ return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" '
107
+ f'fill="{color}" stroke="none">{body}</svg>')
108
+ return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" '
109
+ f'stroke="{color}" stroke-width="{stroke_width}" stroke-linecap="round" '
110
+ f'stroke-linejoin="round">{body}</svg>')
111
+
112
+
113
+ _cache: dict[tuple, QIcon] = {}
114
+
115
+
116
+ def render_pixmap(name: str, color: str, size: int = 22, stroke_width: float = 1.7,
117
+ ratio: float = 2.0) -> QPixmap:
118
+ px = QPixmap(int(size * ratio), int(size * ratio))
119
+ px.setDevicePixelRatio(ratio)
120
+ px.fill(Qt.transparent)
121
+ if _HAVE_SVG:
122
+ renderer = QSvgRenderer(QByteArray(_svg(name, color, stroke_width).encode()))
123
+ p = QPainter(px)
124
+ p.setRenderHint(QPainter.Antialiasing, True)
125
+ renderer.render(p, QRectF(0, 0, size, size))
126
+ p.end()
127
+ else: # pragma: no cover - fallback shape
128
+ p = QPainter(px)
129
+ p.setRenderHint(QPainter.Antialiasing, True)
130
+ p.setBrush(QColor(color))
131
+ p.setPen(Qt.NoPen)
132
+ p.drawRoundedRect(QRectF(3, 3, size - 6, size - 6), 5, 5)
133
+ p.end()
134
+ return px
135
+
136
+
137
+ def icon(name: str, color: str, size: int = 22, stroke_width: float = 1.7) -> QIcon:
138
+ key = (name, color, size, round(stroke_width, 2))
139
+ if key not in _cache:
140
+ _cache[key] = QIcon(render_pixmap(name, color, size, stroke_width))
141
+ return _cache[key]
142
+
143
+
144
+ def clear_cache() -> None:
145
+ _cache.clear()
@@ -0,0 +1,291 @@
1
+ """Token -> QSS generator and the ThemeManager that applies it app-wide."""
2
+ from __future__ import annotations
3
+
4
+ from PySide6.QtCore import QObject, Signal
5
+ from PySide6.QtGui import QColor, QPalette
6
+ from PySide6.QtWidgets import QApplication
7
+
8
+ from .tokens import Theme, get_theme
9
+
10
+
11
+ def _qc(spec: str) -> QColor:
12
+ if spec.startswith("rgba("):
13
+ n = [int(float(x)) for x in spec[5:-1].split(",")]
14
+ return QColor(n[0], n[1], n[2], n[3] if len(n) > 3 else 255)
15
+ return QColor(spec)
16
+
17
+
18
+ _FONT_STACK: str | None = None
19
+
20
+
21
+ def _ui_font_stack() -> str:
22
+ """A font-family list whose FIRST entry is actually installed on this machine.
23
+
24
+ Leading with a present family avoids Qt's slow missing-family alias lookup (the
25
+ 'Populating font family aliases … Replace uses of missing font family "Inter"'
26
+ warning + delay seen on macOS, which lacks Inter). Inter is still used when present.
27
+ """
28
+ global _FONT_STACK
29
+ if _FONT_STACK is not None:
30
+ return _FONT_STACK
31
+ prefs = ["Inter", "SF Pro Text", "Helvetica Neue", "Segoe UI", "Roboto", "Arial"]
32
+ try:
33
+ from PySide6.QtGui import QFontDatabase
34
+ avail = set(QFontDatabase.families())
35
+ present = [f for f in prefs if f in avail]
36
+ except Exception:
37
+ present = []
38
+ present = present[:4] or ["Helvetica Neue"]
39
+ _FONT_STACK = ", ".join(f'"{f}"' for f in present) + ", sans-serif"
40
+ return _FONT_STACK
41
+
42
+
43
+ def build_palette(t: Theme) -> QPalette:
44
+ """A full palette so even unstyled surfaces (scroll viewports, tab panes,
45
+ plain QWidgets) follow the theme instead of falling back to system light."""
46
+ p = QPalette()
47
+ white = QColor("#ffffff")
48
+ p.setColor(QPalette.Window, _qc(t.bg))
49
+ p.setColor(QPalette.WindowText, _qc(t.text))
50
+ p.setColor(QPalette.Base, _qc(t.bg3))
51
+ p.setColor(QPalette.AlternateBase, _qc(t.bg2))
52
+ p.setColor(QPalette.Text, _qc(t.text))
53
+ p.setColor(QPalette.Button, _qc(t.panel2))
54
+ p.setColor(QPalette.ButtonText, _qc(t.text))
55
+ p.setColor(QPalette.ToolTipBase, _qc(t.panel2))
56
+ p.setColor(QPalette.ToolTipText, _qc(t.text))
57
+ p.setColor(QPalette.PlaceholderText, _qc(t.faint))
58
+ p.setColor(QPalette.Highlight, _qc(t.accent))
59
+ p.setColor(QPalette.HighlightedText, white)
60
+ p.setColor(QPalette.BrightText, white)
61
+ p.setColor(QPalette.Link, _qc(t.accent))
62
+ for role in (QPalette.Text, QPalette.WindowText, QPalette.ButtonText):
63
+ p.setColor(QPalette.Disabled, role, _qc(t.faint))
64
+ return p
65
+
66
+
67
+ # UI text-size presets (Settings → Appearance). A single scale factor multiplies the base font
68
+ # sizes in the stylesheet, so the whole UI grows/shrinks together.
69
+ FONT_SCALES = {"Normal": 1.0, "Large": 1.16, "Extra Large": 1.32}
70
+
71
+
72
+ def scale_for(label: str) -> float:
73
+ return FONT_SCALES.get((label or "Normal").strip(), 1.0)
74
+
75
+
76
+ # The active UI text scale, published module-wide so custom-painted widgets (canvas cards, the
77
+ # dashboard, palette headers, toolbar pills) that set their own point sizes can honour the setting.
78
+ _ACTIVE_SCALE = 1.0
79
+
80
+
81
+ def ui_scale() -> float:
82
+ return _ACTIVE_SCALE
83
+
84
+
85
+ def sp(base_pt: float) -> int:
86
+ """A point size scaled by the active UI text-size setting (never below 1)."""
87
+ return max(1, round(base_pt * _ACTIVE_SCALE))
88
+
89
+
90
+ import re as _re
91
+
92
+ _FS_RE = _re.compile(r"(font-size:\s*)(\d+)px")
93
+
94
+
95
+ def scale_css(css: str) -> str:
96
+ """Scale every `font-size: Npx` in a stylesheet by the active UI text size, leaving all other
97
+ px (padding, borders, radii) untouched. Lets widgets that set their own stylesheet honour the
98
+ Settings → Text size choice without hand-editing each size."""
99
+ if abs(_ACTIVE_SCALE - 1.0) < 1e-3:
100
+ return css
101
+ return _FS_RE.sub(lambda m: f"{m.group(1)}{max(1, round(int(m.group(2)) * _ACTIVE_SCALE))}px", css)
102
+
103
+
104
+ def build_qss(t: Theme, scale: float = 1.0) -> str:
105
+ base = round(13 * scale) # the app-wide default size
106
+ small = round(11 * scale) # the smaller section-header size
107
+ return f"""
108
+ * {{
109
+ font-family: {_ui_font_stack()};
110
+ font-size: {base}px;
111
+ color: {t.text};
112
+ outline: none;
113
+ }}
114
+ QMainWindow, QDialog {{ background: {t.bg}; }}
115
+ QWidget#Sidebar, QWidget#Inspector, QWidget#Console {{ background: {t.panel}; }}
116
+ QFrame#Card {{ background: {t.panel2}; border: 1px solid {t.line}; border-radius: 10px; }}
117
+
118
+ QToolBar {{
119
+ background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {t.panel}, stop:1 {t.panel2});
120
+ border: none; border-bottom: 1px solid {t.line2};
121
+ spacing: 4px; padding: 7px 12px;
122
+ }}
123
+ QToolBar::separator {{ background: {t.line}; width: 1px; margin: 6px 8px; border-radius: 1px; }}
124
+
125
+ QToolButton, QPushButton {{
126
+ background: transparent; color: {t.muted};
127
+ border: 1px solid transparent; border-radius: 8px; padding: 7px 10px;
128
+ font-size: {base}px; /* explicit so buttons follow the text-size setting */
129
+ }}
130
+ QToolButton:hover, QPushButton:hover {{ background: {t.bg3}; color: {t.text}; border-color: {t.line2}; }}
131
+ QToolButton:pressed, QPushButton:pressed {{ background: {t.bg2}; }}
132
+ QToolButton:checked {{ background: {t.accent_soft}; color: {t.accent}; border-color: {t.accent}; }}
133
+
134
+ /* segmented toolbar clusters — File / Tools / Zoom sit in rounded inset trays */
135
+ QFrame#TbGroup {{ background: {t.bg2}; border: 1px solid {t.line}; border-radius: 10px; }}
136
+ QFrame#TbGroup QToolButton {{
137
+ background: transparent; color: {t.muted};
138
+ border: 1px solid transparent; border-radius: 7px; padding: 6px 8px;
139
+ }}
140
+ QFrame#TbGroup QToolButton:hover {{ background: {t.panel}; color: {t.text}; border-color: {t.line2}; }}
141
+ QFrame#TbGroup QToolButton:pressed {{ background: {t.bg3}; }}
142
+ QFrame#TbGroup QToolButton:checked {{
143
+ background: {t.accent_soft}; color: {t.accent}; border-color: {t.accent}; }}
144
+
145
+ /* Run/Stop is now a single custom-painted circular RunButton (ui/run_button.py). */
146
+
147
+ /* Project navigator — a centred chip in the toolbar that names the current project. */
148
+ QToolButton#NavBtn {{
149
+ background: {t.panel2}; color: {t.text}; font-weight: 600;
150
+ border: 1px solid {t.line}; border-radius: 9px;
151
+ padding: 5px 14px 5px 10px;
152
+ }}
153
+ QToolButton#NavBtn:hover {{ border-color: {t.accent}; color: {t.accent}; }}
154
+ QToolButton#NavBtn::menu-indicator {{ image: none; width: 0; }}
155
+
156
+ QPushButton#Primary {{ background: {t.success}; color: #ffffff; border: none; font-weight: 500; }}
157
+ QPushButton#Primary:hover {{ background: {t.success}; }}
158
+ QPushButton#Danger {{ color: {t.danger}; }}
159
+ QPushButton#Accent {{ background: {t.accent}; color: #ffffff; border: none; font-weight: 500; }}
160
+
161
+ /* GINI mode buttons — clear pressed/unpressed (toggle) state with an accent glow */
162
+ QPushButton#ModeBtn {{
163
+ background: {t.bg3}; color: {t.muted};
164
+ border: 1px solid {t.line}; border-radius: 13px;
165
+ padding: 5px 12px; font-weight: 500; font-size: {base}px;
166
+ }}
167
+ QPushButton#ModeBtn:hover {{ color: {t.text}; border-color: {t.accent}; }}
168
+ QPushButton#ModeBtn:checked {{
169
+ background: {t.accent_soft}; color: {t.accent};
170
+ border: 1px solid {t.accent}; font-weight: 600;
171
+ }}
172
+ QPushButton#WizardBtn:checked {{
173
+ background: {t.accent_soft}; color: {t.accent2}; border: 1px solid {t.accent2};
174
+ }}
175
+
176
+ /* small toggle chips — requirement chips in the Wizard, follow-up suggestions */
177
+ QPushButton#Chip {{
178
+ background: {t.bg3}; color: {t.muted};
179
+ border: 1px solid {t.line}; border-radius: 12px;
180
+ padding: 4px 11px; font-weight: 500; font-size: {base}px;
181
+ }}
182
+ QPushButton#Chip:hover {{ color: {t.text}; border-color: {t.accent}; }}
183
+ QPushButton#Chip:checked {{
184
+ background: {t.accent_soft}; color: {t.accent};
185
+ border: 1px solid {t.accent}; font-weight: 600;
186
+ }}
187
+
188
+ /* Wizard goal banner — the "🎯 Building: …" objective card */
189
+ QWidget#GoalBanner {{
190
+ background: {t.accent_soft}; border: 1.4px solid {t.accent}; border-radius: 11px;
191
+ }}
192
+
193
+ QLineEdit, QPlainTextEdit, QTextEdit, QComboBox, QSpinBox {{
194
+ background: {t.bg3}; color: {t.text};
195
+ border: 1px solid {t.line}; border-radius: 7px; padding: 6px 9px;
196
+ selection-background-color: {t.accent};
197
+ }}
198
+ QLineEdit:focus, QPlainTextEdit:focus, QTextEdit:focus, QComboBox:focus {{ border-color: {t.accent}; }}
199
+ QComboBox::drop-down {{ border: none; width: 18px; }}
200
+
201
+ QLabel#PanelHead {{ color: {t.faint}; font-size: {small}px; font-weight: 600;
202
+ letter-spacing: 1px; text-transform: uppercase; }}
203
+ QLabel#Muted {{ color: {t.muted}; }}
204
+ QLabel#Faint {{ color: {t.faint}; }}
205
+
206
+ QListWidget, QTreeWidget, QTreeView, QListView {{
207
+ background: transparent; border: none; }}
208
+ QListWidget::item, QTreeWidget::item {{
209
+ border-radius: 7px; padding: 5px 6px; margin: 1px 2px; color: {t.text}; }}
210
+ QListWidget::item:hover, QTreeWidget::item:hover {{ background: {t.bg3}; }}
211
+ QListWidget::item:selected, QTreeWidget::item:selected {{
212
+ background: {t.accent_soft}; color: {t.text}; }}
213
+ QHeaderView::section {{ background: transparent; color: {t.faint};
214
+ border: none; border-bottom: 1px solid {t.line}; padding: 4px 6px; }}
215
+
216
+ QScrollArea {{ background: transparent; border: none; }}
217
+ QScrollArea > QWidget > QWidget {{ background: transparent; }}
218
+ QAbstractScrollArea {{ background: transparent; }}
219
+ QTabWidget::pane {{ background: {t.panel}; border: none; border-top: 1px solid {t.line}; }}
220
+ QTabWidget > QWidget {{ background: {t.panel}; }}
221
+ QTabBar::tab {{ background: transparent; color: {t.muted}; padding: 7px 12px;
222
+ border: none; border-bottom: 2px solid transparent; }}
223
+ QTabBar::tab:selected {{ color: {t.text}; border-bottom-color: {t.accent}; }}
224
+ QTabBar::tab:hover {{ color: {t.text}; }}
225
+
226
+ QDockWidget {{ titlebar-close-icon: none; titlebar-normal-icon: none; }}
227
+ QDockWidget::title {{ background: {t.panel}; color: {t.faint};
228
+ padding: 8px 12px; border-bottom: 1px solid {t.line}; }}
229
+
230
+ QMenuBar {{ background: {t.panel}; border-bottom: 1px solid {t.line}; padding: 3px 6px; }}
231
+ QMenuBar::item {{ background: transparent; padding: 5px 10px; border-radius: 6px; color: {t.muted}; }}
232
+ QMenuBar::item:selected {{ background: {t.bg3}; color: {t.text}; }}
233
+ QMenu {{ background: {t.panel2}; border: 1px solid {t.line}; border-radius: 8px; padding: 5px; }}
234
+ QMenu::item {{ padding: 6px 22px 6px 14px; border-radius: 6px; color: {t.text}; }}
235
+ QMenu::item:selected {{ background: {t.accent_soft}; }}
236
+
237
+ QStatusBar {{ background: {t.panel}; border-top: 1px solid {t.line}; color: {t.muted}; }}
238
+ QStatusBar::item {{ border: none; }}
239
+
240
+ QScrollBar:vertical {{ background: transparent; width: 10px; margin: 2px; }}
241
+ QScrollBar::handle:vertical {{ background: {t.line2}; border-radius: 5px; min-height: 24px; }}
242
+ QScrollBar::handle:vertical:hover {{ background: {t.muted}; }}
243
+ QScrollBar:horizontal {{ background: transparent; height: 10px; margin: 2px; }}
244
+ QScrollBar::handle:horizontal {{ background: {t.line2}; border-radius: 5px; min-width: 24px; }}
245
+ QScrollBar::add-line, QScrollBar::sub-line {{ height: 0; width: 0; }}
246
+ QScrollBar::add-page, QScrollBar::sub-page {{ background: transparent; }}
247
+ QAbstractScrollArea::corner {{ background: {t.bg}; border: none; }}
248
+
249
+ QToolTip {{ background: {t.panel2}; color: {t.text};
250
+ border: 1px solid {t.line}; border-radius: 6px; padding: 5px 8px; }}
251
+ QSplitter::handle {{ background: {t.line}; }}
252
+ """
253
+
254
+
255
+ class ThemeManager(QObject):
256
+ themeChanged = Signal(str)
257
+
258
+ def __init__(self, app: QApplication, name: str = "dark", font_scale: float = 1.0) -> None:
259
+ super().__init__()
260
+ self._app = app
261
+ self.theme: Theme = get_theme(name)
262
+ self.font_scale = font_scale or 1.0
263
+ # Fusion respects QPalette + QSS identically on macOS/Linux/Windows, so the
264
+ # dark theme reaches every widget (the native macOS style does not).
265
+ self._app.setStyle("Fusion")
266
+
267
+ def apply(self) -> None:
268
+ global _ACTIVE_SCALE
269
+ _ACTIVE_SCALE = self.font_scale # publish for custom-painted widgets (canvas, etc.)
270
+ # Scale the app font too, so widgets that read the application font (and any not pinned by
271
+ # the stylesheet) grow with the setting, not just the QSS-styled ones.
272
+ f = self._app.font()
273
+ f.setPointSizeF(10.0 * self.font_scale)
274
+ self._app.setFont(f)
275
+ self._app.setPalette(build_palette(self.theme))
276
+ self._app.setStyleSheet(build_qss(self.theme, self.font_scale))
277
+
278
+ def set_theme(self, name: str) -> None:
279
+ self.theme = get_theme(name)
280
+ self.apply()
281
+ self.themeChanged.emit(self.theme.name)
282
+
283
+ def set_font_scale(self, scale: float) -> None:
284
+ """Change the UI text size live (Settings → Appearance)."""
285
+ if abs((scale or 1.0) - self.font_scale) < 1e-3:
286
+ return
287
+ self.font_scale = scale or 1.0
288
+ self.apply()
289
+ # nudge every theme-aware widget to re-render at the new size (palette headers, dashboard,
290
+ # canvas cards, pills all re-style / repaint on this signal).
291
+ self.themeChanged.emit(self.theme.name)
@@ -0,0 +1,194 @@
1
+ """Themeable design tokens.
2
+
3
+ A Theme is a flat bag of color tokens. The QSS generator and the custom-painted
4
+ canvas both read from it, so re-theming the whole app is a matter of swapping one
5
+ Theme object. New themes can be added by appending to THEMES — including ones
6
+ loaded from JSON at runtime.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Theme:
15
+ name: str
16
+ dark: bool
17
+ # surfaces
18
+ bg: str
19
+ bg2: str
20
+ bg3: str
21
+ panel: str
22
+ panel2: str
23
+ # borders
24
+ line: str
25
+ line2: str
26
+ # text
27
+ text: str
28
+ muted: str
29
+ faint: str
30
+ # primary accent
31
+ accent: str
32
+ accent2: str
33
+ accent_soft: str
34
+ # semantic
35
+ success: str
36
+ success_soft: str
37
+ warning: str
38
+ warning_soft: str
39
+ danger: str
40
+ danger_soft: str
41
+ # canvas
42
+ grid: str
43
+ # category accents (icon recolor + node accent), keyed by Accent.value
44
+ accents: dict[str, str] = field(default_factory=dict)
45
+ # fill for element cards ON the canvas — a lighter, theme-tinted surface so nodes
46
+ # pop off the background (empty = fall back to panel2, i.e. the dark themes' behaviour)
47
+ node: str = ""
48
+ # depth + motion
49
+ shadow: str = "rgba(0,0,0,120)"
50
+ elevation: int = 22 # node drop-shadow blur radius (deeper = more depth)
51
+ dur_fast: int = 120 # ms — hover/press
52
+ dur_base: int = 200 # ms — transitions
53
+
54
+ def accent_for(self, key: str) -> str:
55
+ return self.accents.get(key, self.accent)
56
+
57
+ def node_fill(self) -> str:
58
+ """The canvas element-card fill (see `node`)."""
59
+ return self.node or self.panel2
60
+
61
+
62
+ _CATEGORY_DARK = {
63
+ "blue": "#4c8dff", "green": "#3fb950", "purple": "#a371f7", "teal": "#39c5cf",
64
+ "cyan": "#2dd4bf", "indigo": "#7c8cff", "amber": "#e3a008", "pink": "#f778ba",
65
+ "slate": "#8b98a8", "orange": "#f0883e", "red": "#f85149",
66
+ }
67
+ _CATEGORY_LIGHT = {
68
+ "blue": "#2f6fe0", "green": "#1a7f37", "purple": "#8250df", "teal": "#1b7c83",
69
+ "cyan": "#0e9384", "indigo": "#4f57d2", "amber": "#9a6700", "pink": "#bf3989",
70
+ "slate": "#57606a", "orange": "#bc4c00", "red": "#cf222e",
71
+ }
72
+ _CATEGORY_BRAND = {
73
+ "blue": "#5aa0ff", "green": "#46c98a", "purple": "#b78cff", "teal": "#43d6c4",
74
+ "cyan": "#34e0c8", "indigo": "#8aa0ff", "amber": "#f0b429", "pink": "#ff8ad0",
75
+ "slate": "#9fb0c8", "orange": "#ff9e4f", "red": "#ff6b66",
76
+ }
77
+
78
+
79
+ DARK = Theme(
80
+ name="Dark", dark=True,
81
+ bg="#0e1116", bg2="#151a21", bg3="#1b212b", panel="#11161d", panel2="#161c24",
82
+ line="#232b36", line2="#2d3744",
83
+ text="#e6edf3", muted="#9aa7b4", faint="#697682",
84
+ accent="#4c8dff", accent2="#2f6fe0", accent_soft="rgba(76,141,255,40)",
85
+ success="#3fb950", success_soft="rgba(63,185,80,38)",
86
+ warning="#d29922", warning_soft="rgba(210,153,34,38)",
87
+ danger="#f85149", danger_soft="rgba(248,81,73,38)",
88
+ grid="rgba(255,255,255,12)",
89
+ shadow="rgba(0,0,0,150)",
90
+ accents=_CATEGORY_DARK,
91
+ )
92
+
93
+ LIGHT = Theme(
94
+ name="Light", dark=False,
95
+ bg="#eef1f5", bg2="#e7ebf1", bg3="#dfe4ec", panel="#ffffff", panel2="#f6f8fb",
96
+ line="#d7dde6", line2="#c4ccd8",
97
+ text="#131a23", muted="#3f4b5a", faint="#5e6b7b",
98
+ accent="#2f6fe0", accent2="#2560c8", accent_soft="rgba(47,111,224,28)",
99
+ success="#1a7f37", success_soft="rgba(26,127,55,26)",
100
+ warning="#9a6700", warning_soft="rgba(154,103,0,26)",
101
+ danger="#cf222e", danger_soft="rgba(207,34,46,26)",
102
+ grid="rgba(20,30,50,16)",
103
+ shadow="rgba(60,80,120,50)",
104
+ node="#fdfeff",
105
+ accents=_CATEGORY_LIGHT,
106
+ )
107
+
108
+ BRAND = Theme(
109
+ name="GINI Brand", dark=True,
110
+ bg="#0b1020", bg2="#121833", bg3="#18204a", panel="#0e1428", panel2="#141c3a",
111
+ line="#23305c", line2="#2f3e74",
112
+ text="#eaf0ff", muted="#9fb0d6", faint="#6f80ad",
113
+ accent="#43d6c4", accent2="#2bb6a6", accent_soft="rgba(67,214,196,40)",
114
+ success="#46c98a", success_soft="rgba(70,201,138,38)",
115
+ warning="#f0b429", warning_soft="rgba(240,180,41,38)",
116
+ danger="#ff6b6b", danger_soft="rgba(255,107,107,38)",
117
+ grid="rgba(120,160,255,16)",
118
+ shadow="rgba(0,0,25,160)",
119
+ accents=_CATEGORY_BRAND,
120
+ )
121
+
122
+ HIGH_CONTRAST = Theme(
123
+ name="High Contrast", dark=True,
124
+ bg="#000000", bg2="#0a0a0a", bg3="#161616", panel="#000000", panel2="#0d0d0d",
125
+ line="#5a5a5a", line2="#7c7c7c",
126
+ text="#ffffff", muted="#d6d6d6", faint="#a6a6a6",
127
+ accent="#5aa3ff", accent2="#3a86ff", accent_soft="rgba(90,163,255,60)",
128
+ success="#3fe06a", success_soft="rgba(63,224,106,60)",
129
+ warning="#ffcc33", warning_soft="rgba(255,204,51,60)",
130
+ danger="#ff5a5a", danger_soft="rgba(255,90,90,60)",
131
+ grid="rgba(255,255,255,30)",
132
+ shadow="rgba(0,0,0,210)",
133
+ accents={"blue": "#5aa3ff", "green": "#3fe06a", "purple": "#c08cff",
134
+ "teal": "#3fe0d0", "cyan": "#3fe0c0", "indigo": "#9aa8ff",
135
+ "amber": "#ffcc33", "pink": "#ff7ad0", "slate": "#c8d0d8",
136
+ "orange": "#ff9e4f", "red": "#ff6b66"},
137
+ )
138
+
139
+
140
+ # --- light family: warmer/cooler tints of the Light theme ------------------- #
141
+ SAND = Theme(
142
+ name="Sand", dark=False,
143
+ bg="#f4efe4", bg2="#ece5d6", bg3="#e3dac7", panel="#fdfaf3", panel2="#f4efe4",
144
+ line="#ddd2be", line2="#cabca3",
145
+ text="#2b2418", muted="#5d5240", faint="#7e715c",
146
+ accent="#b46e28", accent2="#985c1c", accent_soft="rgba(180,110,40,30)",
147
+ success="#4c7a2f", success_soft="rgba(76,122,47,26)",
148
+ warning="#9a6700", warning_soft="rgba(154,103,0,26)",
149
+ danger="#c0392b", danger_soft="rgba(192,57,43,26)",
150
+ grid="rgba(90,70,35,16)",
151
+ shadow="rgba(90,70,40,55)",
152
+ node="#fffdf6",
153
+ accents=_CATEGORY_LIGHT,
154
+ )
155
+
156
+ BLUE = Theme(
157
+ name="Blue", dark=False,
158
+ bg="#eef2f9", bg2="#e4ecf6", bg3="#d9e3f2", panel="#ffffff", panel2="#f3f7fc",
159
+ line="#d2ddec", line2="#becde2",
160
+ text="#111a2b", muted="#3c4a63", faint="#5b6a85",
161
+ accent="#2f6fe0", accent2="#2560c8", accent_soft="rgba(47,111,224,28)",
162
+ success="#1a7f37", success_soft="rgba(26,127,55,26)",
163
+ warning="#9a6700", warning_soft="rgba(154,103,0,26)",
164
+ danger="#cf222e", danger_soft="rgba(207,34,46,26)",
165
+ grid="rgba(30,60,120,16)",
166
+ shadow="rgba(40,70,130,55)",
167
+ node="#fafdff",
168
+ accents=_CATEGORY_LIGHT,
169
+ )
170
+
171
+ GREEN = Theme(
172
+ name="Green", dark=False,
173
+ bg="#eef4ee", bg2="#e4eee4", bg3="#d8e7d8", panel="#fbfdfb", panel2="#f0f7f0",
174
+ line="#d1e0d1", line2="#bcd0bc",
175
+ text="#14231a", muted="#3e5245", faint="#5d7163",
176
+ accent="#2e8b57", accent2="#237045", accent_soft="rgba(46,139,87,28)",
177
+ success="#1a7f37", success_soft="rgba(26,127,55,26)",
178
+ warning="#9a6700", warning_soft="rgba(154,103,0,26)",
179
+ danger="#cf222e", danger_soft="rgba(207,34,46,26)",
180
+ grid="rgba(30,90,50,16)",
181
+ shadow="rgba(40,90,60,50)",
182
+ node="#f9fefb",
183
+ accents=_CATEGORY_LIGHT,
184
+ )
185
+
186
+
187
+ THEMES: dict[str, Theme] = {t.name.lower(): t for t in
188
+ (DARK, LIGHT, BRAND, HIGH_CONTRAST, SAND, BLUE, GREEN)}
189
+ # friendly aliases
190
+ THEMES["brand"] = BRAND
191
+
192
+
193
+ def get_theme(name: str) -> Theme:
194
+ return THEMES.get(name.lower(), DARK)