ui-design-workbench-cli 0.5.0__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 (73) hide show
  1. android_resource_resolver.py +370 -0
  2. android_xml_support.py +230 -0
  3. apple_resource_resolver.py +141 -0
  4. coverage_report.py +134 -0
  5. fidelity_adapter_api.py +89 -0
  6. fidelity_adapters.py +508 -0
  7. fidelity_core.py +241 -0
  8. fidelity_platform_adapters.py +902 -0
  9. ir_contracts.py +76 -0
  10. native_render_android.py +160 -0
  11. native_render_apple.py +165 -0
  12. native_render_contracts.py +148 -0
  13. native_render_registry.py +86 -0
  14. quality_common.py +217 -0
  15. render_preview.py +1797 -0
  16. scan_ui.py +1485 -0
  17. scoped_context.py +458 -0
  18. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/SKILL.md +52 -0
  19. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/adapter-api.md +20 -0
  20. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/agent-integrations.md +39 -0
  21. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/cache-protocol.md +61 -0
  22. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/design-modes.md +109 -0
  23. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/fidelity.md +53 -0
  24. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ir-schema.md +412 -0
  25. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/native-rendering.md +40 -0
  26. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-profiles.json +84 -0
  27. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-standards.md +131 -0
  28. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platforms.md +40 -0
  29. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/quality-automation.md +77 -0
  30. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/review-workflow.md +135 -0
  31. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-guidance-mode.md +22 -0
  32. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-policy-schema.md +83 -0
  33. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-reviewer.md +175 -0
  34. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/validation.md +34 -0
  35. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/workbench-ui.md +108 -0
  36. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/native-render-state.schema.json +71 -0
  37. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-agent-job.schema.json +36 -0
  38. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-graph.schema.json +46 -0
  39. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.patch.schema.json +36 -0
  40. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.schema.json +296 -0
  41. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/uidw-config.schema.json +68 -0
  42. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_resource_resolver.py +370 -0
  43. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_xml_support.py +230 -0
  44. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/apple_resource_resolver.py +141 -0
  45. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/coverage_report.py +134 -0
  46. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapter_api.py +89 -0
  47. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapters.py +508 -0
  48. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_core.py +241 -0
  49. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_platform_adapters.py +902 -0
  50. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/generate_interaction_matrix.py +166 -0
  51. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/ir_contracts.py +76 -0
  52. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/merge_review_state.py +267 -0
  53. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_android.py +160 -0
  54. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_apple.py +165 -0
  55. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_contracts.py +148 -0
  56. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_registry.py +86 -0
  57. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/quality_common.py +217 -0
  58. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/render_preview.py +1797 -0
  59. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scan_ui.py +1485 -0
  60. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scoped_context.py +458 -0
  61. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/smoke_preview.js +978 -0
  62. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw.py +2821 -0
  63. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw_mcp.py +282 -0
  64. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/validate_platform_profiles.py +93 -0
  65. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/visual_regression.py +141 -0
  66. ui_design_workbench_cli-0.5.0.dist-info/METADATA +133 -0
  67. ui_design_workbench_cli-0.5.0.dist-info/RECORD +73 -0
  68. ui_design_workbench_cli-0.5.0.dist-info/WHEEL +5 -0
  69. ui_design_workbench_cli-0.5.0.dist-info/entry_points.txt +3 -0
  70. ui_design_workbench_cli-0.5.0.dist-info/top_level.txt +20 -0
  71. uidw.py +2821 -0
  72. uidw_mcp.py +282 -0
  73. validate_platform_profiles.py +93 -0
render_preview.py ADDED
@@ -0,0 +1,1797 @@
1
+ #!/usr/bin/env python3
2
+ """Render UI IR as a standalone interactive HTML review preview."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import base64
8
+ import copy
9
+ import html
10
+ import json
11
+ import mimetypes
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from ir_contracts import (
18
+ DIAGNOSTICS_ENGINE_VERSION,
19
+ SUPPORTED_ACTION_TYPES,
20
+ browser_interaction_contract,
21
+ interaction_contract,
22
+ )
23
+ from quality_common import hierarchy_issues, navigation_graph_issues, platform_family as catalog_platform_family, profile_catalog
24
+ from fidelity_core import validate_strict_ir
25
+
26
+
27
+ def validate(ir: dict[str, Any]) -> list[str]:
28
+ errors: list[str] = []
29
+ if ir.get("version") != 1:
30
+ errors.append("IR version must be 1")
31
+ screens = ir.get("screens")
32
+ nodes = ir.get("nodes")
33
+ if not isinstance(screens, list) or not screens:
34
+ errors.append("IR must contain at least one screen")
35
+ if not isinstance(nodes, dict):
36
+ errors.append("IR nodes must be an object")
37
+ nodes = {}
38
+ scenario_fixtures = ir.get("scenarioFixtures", {})
39
+ if not isinstance(scenario_fixtures, dict):
40
+ errors.append("scenarioFixtures must be an object")
41
+ scenario_fixtures = {}
42
+ theme_config = ir.get("themes", {})
43
+ if theme_config and not isinstance(theme_config, dict):
44
+ errors.append("themes must be an object")
45
+ theme_config = {}
46
+ theme_items = theme_config.get("items", []) if isinstance(theme_config, dict) else []
47
+ if theme_items and not isinstance(theme_items, list):
48
+ errors.append("themes.items must be an array")
49
+ theme_items = []
50
+ theme_ids: set[str] = set()
51
+ for theme_index, theme in enumerate(theme_items):
52
+ if not isinstance(theme, dict):
53
+ errors.append(f"themes.items[{theme_index}] must be an object")
54
+ continue
55
+ theme_id = str(theme.get("id") or "")
56
+ if not theme_id or not theme.get("label") or theme.get("kind") not in {"light", "dark", "custom"}:
57
+ errors.append(f"themes.items[{theme_index}] needs id, label, and light/dark/custom kind")
58
+ continue
59
+ if theme_id in theme_ids:
60
+ errors.append(f"Duplicate theme id: {theme_id}")
61
+ theme_ids.add(theme_id)
62
+ for field in ("tokenOverrides", "nodeOverrides"):
63
+ if not isinstance(theme.get(field, {}), dict):
64
+ errors.append(f"Theme {theme_id} {field} must be an object")
65
+ for node_id in theme.get("nodeOverrides", {}):
66
+ if node_id not in nodes:
67
+ errors.append(f"Theme {theme_id} overrides missing node {node_id}")
68
+ if ir.get("design", {}).get("mode") == "reconstruct" and theme.get("kind") != "light" and not theme.get("sourceRefs"):
69
+ errors.append(f"Reconstructed theme {theme_id} needs sourceRefs; do not invent project themes")
70
+ if theme_items and theme_config.get("defaultThemeId") not in theme_ids:
71
+ errors.append("themes.defaultThemeId must reference a declared theme")
72
+ screen_ids: set[str] = set()
73
+ for index, screen in enumerate(screens or []):
74
+ for key in ("id", "name", "root"):
75
+ if not screen.get(key):
76
+ errors.append(f"screens[{index}] is missing {key}")
77
+ if screen.get("id") in screen_ids:
78
+ errors.append(f"Duplicate screen id: {screen.get('id')}")
79
+ screen_ids.add(screen.get("id", ""))
80
+ if screen.get("root") not in nodes:
81
+ errors.append(f"Screen {screen.get('id')} references missing root node {screen.get('root')}")
82
+ if "navigationFlowId" in screen and (
83
+ not isinstance(screen.get("navigationFlowId"), str) or not screen.get("navigationFlowId", "").strip()
84
+ ):
85
+ errors.append(f"Screen {screen.get('id')} navigationFlowId must be a non-empty string")
86
+ if "navigationEntry" in screen and not isinstance(screen.get("navigationEntry"), bool):
87
+ errors.append(f"Screen {screen.get('id')} navigationEntry must be boolean")
88
+ scenarios = screen.get("scenarios", [])
89
+ if not isinstance(scenarios, list):
90
+ errors.append(f"Screen {screen.get('id')} scenarios must be an array")
91
+ scenarios = []
92
+ scenario_ids: set[str] = set()
93
+ for scenario_index, scenario in enumerate(scenarios):
94
+ if not isinstance(scenario, dict) or not scenario.get("id") or not scenario.get("label"):
95
+ errors.append(f"Screen {screen.get('id')} scenarios[{scenario_index}] needs id and label")
96
+ continue
97
+ scenario_id = str(scenario["id"])
98
+ if scenario_id == "default" or scenario_id in scenario_ids:
99
+ errors.append(f"Screen {screen.get('id')} has invalid or duplicate scenario id {scenario_id}")
100
+ scenario_ids.add(scenario_id)
101
+ fixture_ref = scenario.get("fixtureRef")
102
+ if fixture_ref and fixture_ref not in scenario_fixtures:
103
+ errors.append(f"Screen {screen.get('id')} scenario {scenario_id} references missing fixture {fixture_ref}")
104
+
105
+ def validate_scenario_patch(owner: str, patch: Any) -> None:
106
+ if not isinstance(patch, dict):
107
+ errors.append(f"{owner} must be an object")
108
+ return
109
+ overrides = patch.get("nodeOverrides", {})
110
+ states = patch.get("nodeStates", {})
111
+ if not isinstance(overrides, dict):
112
+ errors.append(f"{owner} nodeOverrides must be an object")
113
+ overrides = {}
114
+ if not isinstance(states, dict):
115
+ errors.append(f"{owner} nodeStates must be an object")
116
+ states = {}
117
+ for node_id in overrides:
118
+ if node_id not in nodes:
119
+ errors.append(f"{owner} overrides missing node {node_id}")
120
+ for node_id, state_name in states.items():
121
+ if node_id not in nodes:
122
+ errors.append(f"{owner} targets missing state node {node_id}")
123
+ elif state_name not in nodes[node_id].get("states", {}):
124
+ errors.append(f"{owner} selects missing state {node_id}:{state_name}")
125
+
126
+ for fixture_id, fixture in scenario_fixtures.items():
127
+ validate_scenario_patch(f"Scenario fixture {fixture_id}", fixture)
128
+ for screen in screens or []:
129
+ for scenario in screen.get("scenarios", []) if isinstance(screen.get("scenarios", []), list) else []:
130
+ if not isinstance(scenario, dict):
131
+ continue
132
+ validate_scenario_patch(f"Screen {screen.get('id')} scenario {scenario.get('id')}", scenario)
133
+ screen_tree = ir.get("screenTree")
134
+ if screen_tree is None:
135
+ errors.append("IR must contain a complete screenTree")
136
+ elif not isinstance(screen_tree, list) or not screen_tree:
137
+ errors.append("screenTree must be a non-empty array")
138
+ else:
139
+ tree_screen_ids: list[str] = []
140
+
141
+ def visit_tree(items: list[Any], path: str) -> None:
142
+ for index, item in enumerate(items):
143
+ if not isinstance(item, dict):
144
+ errors.append(f"{path}[{index}] must be an object")
145
+ continue
146
+ screen_id = item.get("screenId") or item.get("screen")
147
+ if screen_id:
148
+ tree_screen_ids.append(str(screen_id))
149
+ if screen_id not in screen_ids:
150
+ errors.append(f"{path}[{index}] references missing screen {screen_id}")
151
+ continue
152
+ children = item.get("children")
153
+ if not item.get("label") or not isinstance(children, list) or not children:
154
+ errors.append(f"{path}[{index}] group needs label and non-empty children")
155
+ continue
156
+ visit_tree(children, f"{path}[{index}].children")
157
+
158
+ visit_tree(screen_tree, "screenTree")
159
+ duplicates = sorted({item for item in tree_screen_ids if tree_screen_ids.count(item) > 1})
160
+ missing = sorted(screen_ids - set(tree_screen_ids))
161
+ if duplicates:
162
+ errors.append(f"screenTree contains duplicate screen references: {', '.join(duplicates)}")
163
+ if missing:
164
+ errors.append(f"screenTree is missing screens: {', '.join(missing)}")
165
+ for issue in hierarchy_issues(ir):
166
+ if issue["code"] == "group-path-mismatch":
167
+ errors.append(
168
+ f"screenTree path for {issue['screenId']} does not preserve source groups "
169
+ f"{issue['expected']}: {issue['actual']}"
170
+ )
171
+ elif issue["code"] == "parent-hierarchy-flattened":
172
+ errors.append(
173
+ f"screenTree flattens nested screen {issue['screenId']} beside parent "
174
+ f"{issue['parentScreenId']}"
175
+ )
176
+ for issue in navigation_graph_issues(ir):
177
+ errors.append(f"navigationGraph contract failed: {issue}")
178
+ for node_id, node in nodes.items():
179
+ for child_id in node.get("children", []):
180
+ if child_id not in nodes:
181
+ errors.append(f"Node {node_id} references missing child {child_id}")
182
+ action = node.get("action") or {}
183
+ if not isinstance(action, dict):
184
+ errors.append(f"Node {node_id} action must be an object")
185
+ continue
186
+ if action.get("type") == "navigate" and action.get("target") not in screen_ids:
187
+ errors.append(f"Node {node_id} navigates to missing screen {action.get('target')}")
188
+ if action.get("type") in {"set-node-state", "toggle-node-state", "toggle"} and action.get("target") not in nodes:
189
+ errors.append(f"Node {node_id} targets missing state node {action.get('target')}")
190
+ if action.get("type") in {"set-node-state", "toggle-node-state", "toggle"} and not action.get("state"):
191
+ errors.append(f"Node {node_id} action {action.get('type')} requires state")
192
+ if action.get("type") in {"set-node-state", "toggle-node-state", "toggle"} and action.get("target") in nodes and action.get("state") not in nodes[action.get("target")].get("states", {}):
193
+ errors.append(f"Node {node_id} selects missing state {action.get('target')}:{action.get('state')}")
194
+ if action.get("type") and action.get("type") not in SUPPORTED_ACTION_TYPES:
195
+ errors.append(f"Node {node_id} uses unsupported action {action.get('type')}")
196
+
197
+ catalog = ir.get("componentCatalog", {})
198
+ catalog_ids: set[str] = set()
199
+ for index, component in enumerate(catalog.get("components", [])):
200
+ component_id = component.get("id")
201
+ if not component_id:
202
+ errors.append(f"componentCatalog.components[{index}] is missing id")
203
+ elif component_id in catalog_ids:
204
+ errors.append(f"Duplicate component catalog id: {component_id}")
205
+ catalog_ids.add(component_id or "")
206
+
207
+ review = ir.get("review", {})
208
+ versions = review.get("versions", [])
209
+ if review:
210
+ if not isinstance(versions, list) or not versions:
211
+ errors.append("review must contain at least one version")
212
+ versions = []
213
+ version_ids: set[str] = set()
214
+ for index, version in enumerate(versions):
215
+ version_id = version.get("id")
216
+ if not version_id:
217
+ errors.append(f"review.versions[{index}] is missing id")
218
+ continue
219
+ if version_id in version_ids:
220
+ errors.append(f"Duplicate review version id: {version_id}")
221
+ version_ids.add(version_id)
222
+ overrides = version.get("nodeOverrides", {})
223
+ if not isinstance(overrides, dict):
224
+ errors.append(f"Review version {version_id} nodeOverrides must be an object")
225
+ else:
226
+ for node_id in overrides:
227
+ if node_id not in nodes:
228
+ errors.append(f"Review version {version_id} overrides missing node {node_id}")
229
+ for version in versions:
230
+ if version.get("parent") and version.get("parent") not in version_ids:
231
+ errors.append(f"Review version {version.get('id')} references missing parent {version.get('parent')}")
232
+ for key in ("baselineVersion", "activeVersion"):
233
+ if review.get(key) and review.get(key) not in version_ids:
234
+ errors.append(f"review.{key} references missing version {review.get(key)}")
235
+ annotation_ids: set[str] = set()
236
+ for index, annotation in enumerate(review.get("annotations", [])):
237
+ annotation_id = annotation.get("id")
238
+ if not annotation_id:
239
+ errors.append(f"review.annotations[{index}] is missing id")
240
+ elif annotation_id in annotation_ids:
241
+ errors.append(f"Duplicate annotation id: {annotation_id}")
242
+ annotation_ids.add(annotation_id or "")
243
+ if annotation.get("nodeId") and annotation.get("nodeId") not in nodes:
244
+ errors.append(f"Annotation {annotation_id} references missing node {annotation.get('nodeId')}")
245
+ if annotation.get("screenId") and annotation.get("screenId") not in screen_ids:
246
+ errors.append(f"Annotation {annotation_id} references missing screen {annotation.get('screenId')}")
247
+ diagnostics = review.get("diagnostics")
248
+ if diagnostics is not None:
249
+ if not isinstance(diagnostics, dict):
250
+ errors.append("review.diagnostics must be an object")
251
+ else:
252
+ profiles = diagnostics.get("profiles", [])
253
+ scenarios = diagnostics.get("scenarios", [])
254
+ if not isinstance(profiles, list) or not profiles:
255
+ errors.append("review.diagnostics.profiles must be a non-empty array")
256
+ profiles = []
257
+ if not isinstance(scenarios, list) or not scenarios:
258
+ errors.append("review.diagnostics.scenarios must be a non-empty array")
259
+ scenarios = []
260
+ profile_ids: set[str] = set()
261
+ for index, profile in enumerate(profiles):
262
+ if not isinstance(profile, dict):
263
+ errors.append(f"review.diagnostics.profiles[{index}] must be an object")
264
+ continue
265
+ profile_id = profile.get("id")
266
+ if not profile_id or not profile.get("label"):
267
+ errors.append(f"review.diagnostics.profiles[{index}] needs id and label")
268
+ if profile_id in profile_ids:
269
+ errors.append(f"Duplicate diagnostics profile id: {profile_id}")
270
+ profile_ids.add(profile_id or "")
271
+ viewport = profile.get("viewport", "current")
272
+ if viewport != "current":
273
+ if not isinstance(viewport, dict) or not isinstance(viewport.get("width"), (int, float)) or not isinstance(viewport.get("height"), (int, float)) or viewport.get("width", 0) <= 0 or viewport.get("height", 0) <= 0:
274
+ errors.append(f"Diagnostics profile {profile_id} viewport must be 'current' or a positive width/height object")
275
+ zoom_levels = profile.get("zoomLevels", [])
276
+ if not isinstance(zoom_levels, list) or not zoom_levels:
277
+ errors.append(f"Diagnostics profile {profile_id} needs non-empty zoomLevels")
278
+ elif any(not isinstance(value, (int, float)) or value < 0.2 or value > 2 for value in zoom_levels):
279
+ errors.append(f"Diagnostics profile {profile_id} zoomLevels must stay between 0.2 and 2")
280
+ allowed_scenarios = {"screen-render", "zoom-reset", "overview-geometry", "menu-exclusivity", "layout-integrity", "accessibility-basics", "state-matrix", "navigation-flow", "contrast-focus"}
281
+ scenario_ids: set[str] = set()
282
+ for index, scenario in enumerate(scenarios):
283
+ if not isinstance(scenario, dict):
284
+ errors.append(f"review.diagnostics.scenarios[{index}] must be an object")
285
+ continue
286
+ scenario_id = scenario.get("id")
287
+ if not scenario_id or not scenario.get("label") or not scenario.get("kind"):
288
+ errors.append(f"review.diagnostics.scenarios[{index}] needs id, label and kind")
289
+ if scenario_id in scenario_ids:
290
+ errors.append(f"Duplicate diagnostics scenario id: {scenario_id}")
291
+ scenario_ids.add(scenario_id or "")
292
+ if scenario.get("kind") not in allowed_scenarios:
293
+ errors.append(f"Diagnostics scenario {scenario_id} uses unsupported kind {scenario.get('kind')}")
294
+ if "entryScreenIds" in scenario:
295
+ entry_ids = scenario.get("entryScreenIds")
296
+ if scenario.get("kind") != "navigation-flow":
297
+ errors.append(f"Diagnostics scenario {scenario_id} may declare entryScreenIds only for navigation-flow")
298
+ if not isinstance(entry_ids, list):
299
+ errors.append(f"Diagnostics scenario {scenario_id} entryScreenIds must be a unique array")
300
+ elif any(not isinstance(entry_id, str) or not entry_id.strip() for entry_id in entry_ids):
301
+ errors.append(f"Diagnostics scenario {scenario_id} entryScreenIds must contain non-empty strings")
302
+ else:
303
+ if len(entry_ids) != len(set(entry_ids)):
304
+ errors.append(f"Diagnostics scenario {scenario_id} entryScreenIds must be a unique array")
305
+ for entry_id in entry_ids:
306
+ if entry_id not in screen_ids:
307
+ errors.append(f"Diagnostics scenario {scenario_id} references missing entry screen {entry_id}")
308
+ audit = review.get("audit")
309
+ if audit is not None:
310
+ if not isinstance(audit, dict):
311
+ errors.append("review.audit must be an object")
312
+ else:
313
+ if not audit.get("summary"):
314
+ errors.append("review.audit is missing summary")
315
+ declared_finding_ids = {
316
+ item.get("id") for item in audit.get("findings", [])
317
+ if isinstance(item, dict) and item.get("id")
318
+ }
319
+ scope = audit.get("scope", {})
320
+ if not isinstance(scope, dict) or not scope.get("tasks") or not scope.get("screens"):
321
+ errors.append("review.audit.scope needs non-empty tasks and screens")
322
+ else:
323
+ for screen_id in scope.get("screens", []):
324
+ if screen_id not in screen_ids:
325
+ errors.append(f"review.audit.scope references missing screen {screen_id}")
326
+ if audit.get("status") == "complete":
327
+ if not scope.get("interactions"):
328
+ errors.append("complete review.audit.scope needs non-empty interactions")
329
+ if not scope.get("uxLenses"):
330
+ errors.append("complete review.audit.scope needs non-empty uxLenses")
331
+ interaction_checks = audit.get("interactionChecks", [])
332
+ if audit.get("status") == "complete" and not interaction_checks:
333
+ errors.append("complete review.audit needs non-empty interactionChecks")
334
+ if not isinstance(interaction_checks, list):
335
+ errors.append("review.audit.interactionChecks must be an array")
336
+ interaction_checks = []
337
+ for index, check in enumerate(interaction_checks):
338
+ if not isinstance(check, dict):
339
+ errors.append(f"review.audit.interactionChecks[{index}] must be an object")
340
+ continue
341
+ for field in ("id", "startState", "actions", "expected", "observed", "result", "viewports", "inputMethods"):
342
+ if not check.get(field):
343
+ errors.append(f"review.audit.interactionChecks[{index}] is missing {field}")
344
+ if check.get("result") not in {"pass", "fail", "not-run"}:
345
+ errors.append(f"Interaction check {check.get('id')} uses unsupported result {check.get('result')}")
346
+ linked_findings = check.get("findingIds", [])
347
+ if check.get("result") == "fail" and not linked_findings:
348
+ errors.append(f"Failed interaction check {check.get('id')} needs findingIds")
349
+ for finding_id in linked_findings:
350
+ if finding_id not in declared_finding_ids:
351
+ errors.append(f"Interaction check {check.get('id')} references missing finding {finding_id}")
352
+ if audit.get("status") == "complete" and check.get("result") == "not-run":
353
+ errors.append(f"Complete audit cannot contain untested interaction check {check.get('id')}")
354
+ layout_checks = audit.get("layoutChecks", [])
355
+ if audit.get("status") == "complete" and not layout_checks:
356
+ errors.append("complete review.audit needs non-empty layoutChecks")
357
+ if not isinstance(layout_checks, list):
358
+ errors.append("review.audit.layoutChecks must be an array")
359
+ layout_checks = []
360
+ required_layout_kinds = {"typography", "sibling-alignment", "control-padding", "text-containment", "icon-label-optics"}
361
+ covered_layout_kinds: set[str] = set()
362
+ for index, check in enumerate(layout_checks):
363
+ if not isinstance(check, dict):
364
+ errors.append(f"review.audit.layoutChecks[{index}] must be an object")
365
+ continue
366
+ for field in ("id", "kind", "scope", "viewports", "zoomLevels", "metrics", "expected", "observed", "result"):
367
+ if not check.get(field):
368
+ errors.append(f"review.audit.layoutChecks[{index}] is missing {field}")
369
+ kind = check.get("kind")
370
+ if kind not in required_layout_kinds:
371
+ errors.append(f"Layout check {check.get('id')} uses unsupported kind {kind}")
372
+ else:
373
+ covered_layout_kinds.add(kind)
374
+ if check.get("result") not in {"pass", "fail", "not-run"}:
375
+ errors.append(f"Layout check {check.get('id')} uses unsupported result {check.get('result')}")
376
+ linked_findings = check.get("findingIds", [])
377
+ if check.get("result") == "fail" and not linked_findings:
378
+ errors.append(f"Failed layout check {check.get('id')} needs findingIds")
379
+ for finding_id in linked_findings:
380
+ if finding_id not in declared_finding_ids:
381
+ errors.append(f"Layout check {check.get('id')} references missing finding {finding_id}")
382
+ if audit.get("status") == "complete" and check.get("result") == "not-run":
383
+ errors.append(f"Complete audit cannot contain untested layout check {check.get('id')}")
384
+ if audit.get("status") == "complete":
385
+ missing_layout_kinds = sorted(required_layout_kinds - covered_layout_kinds)
386
+ if missing_layout_kinds:
387
+ errors.append(f"complete review.audit.layoutChecks misses kinds: {', '.join(missing_layout_kinds)}")
388
+ ux_assessment = audit.get("uxAssessment", [])
389
+ if audit.get("status") == "complete" and not ux_assessment:
390
+ errors.append("complete review.audit needs non-empty uxAssessment")
391
+ if not isinstance(ux_assessment, list):
392
+ errors.append("review.audit.uxAssessment must be an array")
393
+ ux_assessment = []
394
+ for index, assessment in enumerate(ux_assessment):
395
+ if not isinstance(assessment, dict):
396
+ errors.append(f"review.audit.uxAssessment[{index}] must be an object")
397
+ continue
398
+ for field in ("lens", "status", "observation"):
399
+ if not assessment.get(field):
400
+ errors.append(f"review.audit.uxAssessment[{index}] is missing {field}")
401
+ if assessment.get("status") not in {"pass", "finding", "gap"}:
402
+ errors.append(f"UX assessment {assessment.get('lens')} uses unsupported status {assessment.get('status')}")
403
+ linked_findings = assessment.get("findingIds", [])
404
+ if assessment.get("status") == "finding" and not linked_findings:
405
+ errors.append(f"UX assessment {assessment.get('lens')} with finding status needs findingIds")
406
+ for finding_id in linked_findings:
407
+ if finding_id not in declared_finding_ids:
408
+ errors.append(f"UX assessment {assessment.get('lens')} references missing finding {finding_id}")
409
+ findings = audit.get("findings", [])
410
+ if not isinstance(findings, list):
411
+ errors.append("review.audit.findings must be an array")
412
+ findings = []
413
+ finding_ids: set[str] = set()
414
+ allowed_severities = {"blocker", "high", "medium", "low"}
415
+ allowed_confidence = {"high", "medium", "low"}
416
+ allowed_effort = {"small", "medium", "large"}
417
+ allowed_evidence = {
418
+ "requirement", "user-feedback", "research", "analytics", "source",
419
+ "project-pattern", "platform-standard", "accessibility-standard", "heuristic",
420
+ }
421
+ decision_ids = {
422
+ item.get("id") for item in ir.get("design", {}).get("decisions", [])
423
+ if isinstance(item, dict) and item.get("id")
424
+ }
425
+ required_fields = ("id", "title", "category", "severity", "confidence", "screenId", "observation", "impact", "recommendation", "effort")
426
+ for index, finding in enumerate(findings):
427
+ if not isinstance(finding, dict):
428
+ errors.append(f"review.audit.findings[{index}] must be an object")
429
+ continue
430
+ finding_id = finding.get("id")
431
+ for field in required_fields:
432
+ if not finding.get(field):
433
+ errors.append(f"review.audit.findings[{index}] is missing {field}")
434
+ if finding_id in finding_ids:
435
+ errors.append(f"Duplicate review finding id: {finding_id}")
436
+ finding_ids.add(finding_id or "")
437
+ if finding.get("severity") not in allowed_severities:
438
+ errors.append(f"Finding {finding_id} uses unsupported severity {finding.get('severity')}")
439
+ if finding.get("confidence") not in allowed_confidence:
440
+ errors.append(f"Finding {finding_id} uses unsupported confidence {finding.get('confidence')}")
441
+ if finding.get("effort") not in allowed_effort:
442
+ errors.append(f"Finding {finding_id} uses unsupported effort {finding.get('effort')}")
443
+ if finding.get("screenId") not in screen_ids:
444
+ errors.append(f"Finding {finding_id} references missing screen {finding.get('screenId')}")
445
+ if finding.get("nodeId") and finding.get("nodeId") not in nodes:
446
+ errors.append(f"Finding {finding_id} references missing node {finding.get('nodeId')}")
447
+ proposal_id = finding.get("proposalVersionId")
448
+ if proposal_id and proposal_id not in version_ids:
449
+ errors.append(f"Finding {finding_id} references missing proposal version {proposal_id}")
450
+ elif proposal_id:
451
+ proposal = next((item for item in versions if item.get("id") == proposal_id), {})
452
+ if proposal.get("kind") != "proposal":
453
+ errors.append(f"Finding {finding_id} must reference a proposal version")
454
+ if finding_id not in proposal.get("findingIds", []):
455
+ errors.append(f"Proposal version {proposal_id} must link back to finding {finding_id}")
456
+ if not proposal_id and not finding.get("noProposalReason"):
457
+ errors.append(f"Finding {finding_id} needs proposalVersionId or noProposalReason")
458
+ if finding.get("decisionId") and finding.get("decisionId") not in decision_ids:
459
+ errors.append(f"Finding {finding_id} references missing design decision {finding.get('decisionId')}")
460
+ evidence = finding.get("evidence", [])
461
+ if not isinstance(evidence, list) or not evidence:
462
+ errors.append(f"Finding {finding_id} needs at least one evidence entry")
463
+ else:
464
+ for evidence_index, item in enumerate(evidence):
465
+ if not isinstance(item, dict) or item.get("type") not in allowed_evidence or not item.get("ref") or not item.get("note"):
466
+ errors.append(f"Finding {finding_id} has invalid evidence[{evidence_index}]")
467
+ for version in versions:
468
+ for finding_id in version.get("findingIds", []):
469
+ if finding_id not in finding_ids:
470
+ errors.append(f"Review version {version.get('id')} references missing finding {finding_id}")
471
+ for finding_id in version.get("resolvedFindingIds", []):
472
+ if finding_id not in finding_ids:
473
+ errors.append(f"Review version {version.get('id')} resolves missing finding {finding_id}")
474
+ errors.extend(validate_strict_ir(ir))
475
+ return errors
476
+
477
+
478
+ def fidelity_audit(ir: dict[str, Any]) -> dict[str, Any]:
479
+ nodes = ir.get("nodes", {})
480
+ screens = ir.get("screens", [])
481
+ design = ir.get("design", {})
482
+ design_mode = design.get("mode", "reconstruct")
483
+ fidelity = ir.get("fidelity", {})
484
+ meaningful_items = [(node_id, node) for node_id, node in nodes.items() if node.get("type") != "spacer"]
485
+ meaningful = [node for _, node in meaningful_items]
486
+ placeholders = [
487
+ node for node in meaningful
488
+ if node.get("component") in {"UntranslatedSource", "DiscoveredScreen"}
489
+ or (node.get("type") == "custom" and node.get("confidence") == "unsupported")
490
+ ]
491
+ sourced = [node for node in meaningful if node.get("source", {}).get("file")]
492
+ confident = [node for node in meaningful if node.get("confidence") in {"exact", "high"}]
493
+ visual = [node for node in meaningful if node.get("type") in {"text", "button", "input", "image", "icon", "card"}]
494
+ decisions = {item.get("id") for item in design.get("decisions", []) if isinstance(item, dict) and item.get("id")}
495
+ catalog = ir.get("componentCatalog", {})
496
+ catalog_entries = {
497
+ item.get("id"): item for item in catalog.get("components", [])
498
+ if isinstance(item, dict) and item.get("id")
499
+ }
500
+ project_component_nodes = [
501
+ (node_id, node) for node_id, node in meaningful_items
502
+ if node.get("componentRef") or any(item.get("name") == node.get("component") for item in catalog_entries.values())
503
+ ]
504
+ mapped_component_nodes = [
505
+ node for _, node in project_component_nodes
506
+ if node.get("componentRef") in catalog_entries
507
+ and catalog_entries[node.get("componentRef")].get("inspection") == "mapped"
508
+ ]
509
+
510
+ def platform_family(value: Any) -> str | None:
511
+ return catalog_platform_family(value)
512
+
513
+ target_platforms = {platform_family(item) for item in design.get("targetPlatforms", [])}
514
+ target_platforms.discard(None)
515
+ if not target_platforms:
516
+ target_platforms = {platform_family(item) for item in ir.get("platforms", [])}
517
+ target_platforms.discard(None)
518
+ native_platforms = {"android", "ios", "macos", "windows", "flutter"}
519
+ native_evidence_required = bool(target_platforms & native_platforms)
520
+
521
+ node_platforms: dict[str, set[str]] = {node_id: set() for node_id in nodes}
522
+
523
+ def mark_platform(node_id: str, families: set[str], seen: set[str]) -> None:
524
+ if node_id in seen or node_id not in nodes:
525
+ return
526
+ seen.add(node_id)
527
+ node_platforms[node_id].update(families)
528
+ for child_id in nodes[node_id].get("children", []):
529
+ mark_platform(child_id, families, seen)
530
+
531
+ for screen in screens:
532
+ family = platform_family(screen.get("platform"))
533
+ families = {family} if family else set(target_platforms)
534
+ mark_platform(screen.get("root", ""), families, set())
535
+
536
+ parents: dict[str, str] = {}
537
+ for parent_id, node in nodes.items():
538
+ for child_id in node.get("children", []):
539
+ parents.setdefault(child_id, parent_id)
540
+
541
+ def resolve_token(value: Any) -> Any:
542
+ current = value
543
+ seen: set[str] = set()
544
+ android_value = False
545
+ while isinstance(current, str) and current.startswith("$") and current not in seen:
546
+ seen.add(current)
547
+ resolved: Any = ir.get("tokens", {})
548
+ for part in current[1:].split("."):
549
+ if not isinstance(resolved, dict):
550
+ return current
551
+ resolved = resolved.get(part)
552
+ if resolved is None:
553
+ return current
554
+ if isinstance(resolved, dict):
555
+ android_value = android_value or resolved.get("adapter") == "android-xml"
556
+ current = resolved.get("value", resolved)
557
+ else:
558
+ current = resolved
559
+ if android_value and isinstance(current, str) and re.fullmatch(r"#[0-9a-fA-F]{8}", current):
560
+ current = f"#{current[3:]}"
561
+ return current
562
+
563
+ def inherited_style(node_id: str, keys: tuple[str, ...]) -> Any:
564
+ current = node_id
565
+ seen: set[str] = set()
566
+ while current in nodes and current not in seen:
567
+ seen.add(current)
568
+ style = nodes[current].get("style", {})
569
+ for key in keys:
570
+ if style.get(key) is not None:
571
+ return resolve_token(style[key])
572
+ current = parents.get(current, "")
573
+ return None
574
+
575
+ def parse_hex_color(value: Any) -> tuple[float, float, float] | None:
576
+ value = resolve_token(value)
577
+ if not isinstance(value, str) or not value.startswith("#"):
578
+ return None
579
+ raw = value[1:]
580
+ if len(raw) == 3:
581
+ raw = "".join(char * 2 for char in raw)
582
+ if len(raw) != 6:
583
+ return None
584
+ try:
585
+ channels = tuple(int(raw[index:index + 2], 16) / 255 for index in (0, 2, 4))
586
+ except ValueError:
587
+ return None
588
+ return channels
589
+
590
+ def luminance(color: tuple[float, float, float]) -> float:
591
+ converted = [channel / 12.92 if channel <= 0.04045 else ((channel + 0.055) / 1.055) ** 2.4 for channel in color]
592
+ return 0.2126 * converted[0] + 0.7152 * converted[1] + 0.0722 * converted[2]
593
+
594
+ def contrast_ratio(foreground: tuple[float, float, float], background: tuple[float, float, float]) -> float:
595
+ first, second = sorted((luminance(foreground), luminance(background)), reverse=True)
596
+ return (first + 0.05) / (second + 0.05)
597
+
598
+ def appearance_complete(node: dict[str, Any]) -> bool:
599
+ if node.get("inheritsAppearance") is True:
600
+ if design_mode != "reconstruct":
601
+ return True
602
+ component_ref = node.get("componentRef")
603
+ return bool(node.get("appearanceSource") or component_ref in catalog_entries and catalog_entries[component_ref].get("inspection") == "mapped")
604
+ node_type = node.get("type")
605
+ style = node.get("style", {})
606
+ layout = node.get("layout", {})
607
+ if node_type == "text":
608
+ return bool(style.get("typography") or any(key in style for key in ("fontFamily", "fontSize", "fontWeight", "lineHeight")))
609
+ if node_type in {"image", "icon"}:
610
+ sized = ("width" in layout and "height" in layout) or "aspectRatio" in layout
611
+ return bool(node.get("asset") and sized)
612
+ if node_type in {"button", "input"}:
613
+ visual_style = any(key in style for key in ("background", "backgroundColor", "border", "borderColor", "borderWidth", "boxShadow", "shadow"))
614
+ geometry = any(key in layout for key in ("height", "minHeight", "padding", "paddingVertical", "paddingTop", "paddingBottom"))
615
+ typography = bool(style.get("typography") or node.get("inheritsTypography") is True or any(key in style for key in ("fontFamily", "fontSize", "fontWeight", "lineHeight")))
616
+ return visual_style and geometry and typography
617
+ if node_type == "card":
618
+ return any(key in style for key in ("background", "backgroundColor", "border", "borderColor", "boxShadow", "shadow"))
619
+ return True
620
+
621
+ def exclusion_keys(items: list[Any], kind: str) -> tuple[set[str], list[str]]:
622
+ keys: set[str] = set()
623
+ invalid: list[str] = []
624
+ for item in items:
625
+ if not isinstance(item, dict):
626
+ invalid.append(str(item))
627
+ continue
628
+ key = item.get("key")
629
+ if not key and kind == "screen":
630
+ key = f'{item.get("file", "")}#{item.get("name", "")}'
631
+ if not key and kind == "route":
632
+ key = item.get("route")
633
+ if key:
634
+ keys.add(key)
635
+ if not key or not item.get("reason"):
636
+ invalid.append(str(key or item))
637
+ return keys, invalid
638
+
639
+ appearance_complete_nodes = [node for node in visual if appearance_complete(node)]
640
+ excluded_screens, invalid_screen_exclusions = exclusion_keys(fidelity.get("excludedScreens", []), "screen")
641
+ excluded_routes, invalid_route_exclusions = exclusion_keys(fidelity.get("excludedRoutes", []), "route")
642
+ discovered_screens = ir.get("discoveredScreens", [])
643
+ discovered_routes = ir.get("discoveredRoutes", [])
644
+ translated_screen_keys = {
645
+ str(screen.get("discoveredKey") or f'{screen.get("source", {}).get("file", "")}#{screen.get("source", {}).get("symbol", screen.get("name", ""))}')
646
+ for screen in screens
647
+ if screen.get("translationStatus") != "inventory"
648
+ }
649
+ expected_screen_keys = {f'{screen.get("file", "")}#{screen.get("name", "")}' for screen in discovered_screens}
650
+ missing_screen_keys = sorted(expected_screen_keys - translated_screen_keys - excluded_screens)
651
+ translated_routes = {screen.get("route") for screen in screens if screen.get("route")}
652
+ expected_routes = {route.get("route") for route in discovered_routes if route.get("route")}
653
+ missing_routes = sorted(expected_routes - translated_routes - excluded_routes)
654
+ actions = [node.get("action", {}) for node in nodes.values() if node.get("action", {}).get("type")]
655
+ navigation_actions = [action for action in actions if action.get("type") == "navigate"]
656
+ discovered_navigation = ir.get("discoveredNavigationTargets", [])
657
+ excluded_navigation: set[str] = set()
658
+ invalid_navigation_exclusions: list[str] = []
659
+ for item in fidelity.get("excludedNavigationTargets", []):
660
+ if isinstance(item, dict) and item.get("target") and item.get("reason"):
661
+ excluded_navigation.add(str(item["target"]))
662
+ else:
663
+ invalid_navigation_exclusions.append(str(item))
664
+ screen_for_fragment = {
665
+ screen.get("fragment"): screen.get("id") for screen in screens if screen.get("fragment")
666
+ }
667
+ navigated_screen_ids = {action.get("target") for action in navigation_actions}
668
+ navigated_screen_ids.update(
669
+ item.get("screenId") for item in ir.get("navigationGraph", {}).get("navigationTargets", [])
670
+ if isinstance(item, dict) and item.get("screenId")
671
+ )
672
+ expected_navigation_targets = {
673
+ str(item.get("target")) for item in discovered_navigation
674
+ if isinstance(item, dict) and item.get("target")
675
+ }
676
+ missing_navigation_targets = sorted(
677
+ target for target in expected_navigation_targets - excluded_navigation
678
+ if target not in screen_for_fragment or screen_for_fragment[target] not in navigated_screen_ids
679
+ )
680
+ evidence_nodes = [
681
+ node for node in meaningful
682
+ if node.get("source", {}).get("file") or node.get("standardRef") or node.get("standardRefs")
683
+ or node.get("decisionId") in decisions
684
+ ]
685
+ interactive_items = [
686
+ (node_id, node) for node_id, node in meaningful_items
687
+ if interaction_contract(node)["interactive"]
688
+ ]
689
+ semantic_nodes = [
690
+ node for _, node in interactive_items
691
+ if node.get("semantics", {}).get("role") and node.get("semantics", {}).get("label")
692
+ ]
693
+
694
+ platform_catalog = profile_catalog().get("profiles", {})
695
+ expected_profile_ids = {
696
+ str(profile.get("family")): profile_id
697
+ for profile_id, profile in platform_catalog.items()
698
+ if isinstance(profile, dict) and profile.get("family")
699
+ }
700
+ allowed_ref_prefixes = {
701
+ family: tuple(platform_catalog[profile_id].get("standardRefPrefixes", []))
702
+ for family, profile_id in expected_profile_ids.items()
703
+ }
704
+ profiles = design.get("standardProfiles", {})
705
+ missing_profiles: list[str] = []
706
+ invalid_profiles: list[str] = []
707
+ for family in sorted(target_platforms):
708
+ profile = profiles.get(family, {})
709
+ if not profile:
710
+ missing_profiles.append(family)
711
+ elif profile.get("id") not in {expected_profile_ids[family], "project"}:
712
+ invalid_profiles.append(family)
713
+ elif profile.get("id") == "project" and not profile.get("reason"):
714
+ invalid_profiles.append(family)
715
+
716
+ standard_complete: list[dict[str, Any]] = []
717
+ target_complete: list[dict[str, Any]] = []
718
+ target_failures: list[str] = []
719
+ minimum_targets = {
720
+ family: float(platform_catalog[profile_id].get("minimumTarget", {}).get("height", 24))
721
+ for family, profile_id in expected_profile_ids.items()
722
+ }
723
+ for node_id, node in meaningful_items:
724
+ families = node_platforms.get(node_id) or target_platforms
725
+ refs = node.get("standardRefs", {})
726
+ shared_ref = node.get("standardRef", "")
727
+ ref_ok = bool(node.get("decisionId") in decisions)
728
+ if not ref_ok:
729
+ ref_ok = all(
730
+ str(refs.get(family, shared_ref) if isinstance(refs, dict) else shared_ref).startswith(allowed_ref_prefixes[family])
731
+ for family in families
732
+ )
733
+ if ref_ok:
734
+ standard_complete.append(node)
735
+
736
+ for node_id, node in interactive_items:
737
+ families = node_platforms.get(node_id) or target_platforms
738
+ semantics = node.get("semantics", {})
739
+ exception = semantics.get("targetSizeException")
740
+ size = semantics.get("targetSize")
741
+ if exception and isinstance(exception, dict) and exception.get("reason"):
742
+ target_complete.append(node)
743
+ continue
744
+ if isinstance(size, (int, float)):
745
+ width = height = float(size)
746
+ elif isinstance(size, dict):
747
+ width = size.get("width")
748
+ height = size.get("height")
749
+ else:
750
+ width = height = None
751
+ minimum = max((minimum_targets[family] for family in families), default=24)
752
+ if isinstance(width, (int, float)) and isinstance(height, (int, float)) and width >= minimum and height >= minimum:
753
+ target_complete.append(node)
754
+ else:
755
+ target_failures.append(node_id)
756
+
757
+ contrast_items = [
758
+ (node_id, node) for node_id, node in meaningful_items
759
+ if node.get("type") in {"text", "button"} and (node.get("text") or node.get("children"))
760
+ ]
761
+ contrast_resolved: list[str] = []
762
+ contrast_failures: list[dict[str, Any]] = []
763
+ contrast_unresolved: list[str] = []
764
+ for node_id, node in contrast_items:
765
+ exception = node.get("accessibility", {}).get("contrastException")
766
+ if isinstance(exception, dict) and exception.get("reason"):
767
+ contrast_resolved.append(node_id)
768
+ continue
769
+ foreground = parse_hex_color(inherited_style(node_id, ("color",)))
770
+ background = parse_hex_color(inherited_style(node_id, ("backgroundColor", "background")))
771
+ if not foreground or not background:
772
+ contrast_unresolved.append(node_id)
773
+ continue
774
+ style = node.get("style", {})
775
+ typography = resolve_token(style.get("typography"))
776
+ typography = typography if isinstance(typography, dict) else {}
777
+ font_size = resolve_token(style.get("fontSize", typography.get("fontSize", 16)))
778
+ font_weight = resolve_token(style.get("fontWeight", typography.get("fontWeight", 400)))
779
+ large = isinstance(font_size, (int, float)) and (
780
+ font_size >= 24 or (font_size >= 18.66 and isinstance(font_weight, (int, float)) and font_weight >= 700)
781
+ )
782
+ minimum_ratio = 3 if large else 4.5
783
+ ratio = contrast_ratio(foreground, background)
784
+ contrast_resolved.append(node_id)
785
+ if ratio + 1e-9 < minimum_ratio:
786
+ contrast_failures.append({"node": node_id, "ratio": round(ratio, 2), "required": minimum_ratio})
787
+
788
+ required_states: dict[str, set[str]] = {
789
+ screen.get("id", ""): {"default", *{
790
+ str(scenario.get("id")) for scenario in screen.get("scenarios", [])
791
+ if isinstance(scenario, dict) and scenario.get("id")
792
+ }}
793
+ for screen in screens
794
+ }
795
+ addressed_states: dict[str, set[str]] = {screen_id: set() for screen_id in required_states}
796
+ invalid_state_exclusions: list[str] = []
797
+ for entry in design.get("stateMatrix", []):
798
+ if not isinstance(entry, dict):
799
+ invalid_state_exclusions.append(str(entry))
800
+ continue
801
+ entry_screens = entry.get("screens", [])
802
+ if entry.get("screen"):
803
+ entry_screens = [entry["screen"]]
804
+ covered = {str(item) for item in entry.get("covered", [])}
805
+ for item in entry.get("notApplicable", []):
806
+ if isinstance(item, dict) and item.get("state") and item.get("reason"):
807
+ continue
808
+ else:
809
+ invalid_state_exclusions.append(str(item))
810
+ for screen_id in entry_screens:
811
+ if screen_id in addressed_states:
812
+ addressed_states[screen_id].update(covered)
813
+ missing_states = {
814
+ screen_id: sorted(required_states[screen_id] - states)
815
+ for screen_id, states in addressed_states.items()
816
+ if required_states[screen_id] - states
817
+ }
818
+
819
+ source_coverage = round(len(sourced) / len(meaningful), 3) if meaningful else 0
820
+ evidence_coverage = round(len(evidence_nodes) / len(meaningful), 3) if meaningful else 0
821
+ confidence_coverage = round(len(confident) / len(meaningful), 3) if meaningful else 0
822
+ appearance_coverage = round(len(appearance_complete_nodes) / len(visual), 3) if visual else 0
823
+ semantic_coverage = round(len(semantic_nodes) / len(interactive_items), 3) if interactive_items else 1
824
+ standard_coverage = round(len(standard_complete) / len(meaningful_items), 3) if meaningful_items else 0
825
+ target_coverage = round(len(target_complete) / len(interactive_items), 3) if interactive_items else 1
826
+ contrast_coverage = round(len(contrast_resolved) / len(contrast_items), 3) if contrast_items else 1
827
+ total_state_checks = sum(len(states) for states in required_states.values())
828
+ state_coverage = round(
829
+ sum(len(required_states[screen_id] & states) for screen_id, states in addressed_states.items()) / total_state_checks, 3
830
+ ) if total_state_checks else 1
831
+ component_coverage = round(len(mapped_component_nodes) / len(project_component_nodes), 3) if project_component_nodes else 1
832
+ reasons: list[str] = []
833
+ expected_status = "translated" if design_mode == "reconstruct" else "designed"
834
+ if design_mode not in {"reconstruct", "generate", "redesign"}:
835
+ reasons.append(f"Unsupported design mode: {design_mode}.")
836
+ if fidelity.get("status") != expected_status:
837
+ reasons.append(f"Fidelity status must be {expected_status} for {design_mode} mode.")
838
+ if design_mode == "reconstruct" and fidelity.get("sourceDerived") is not True:
839
+ reasons.append("Reconstruction must be explicitly marked sourceDerived.")
840
+ if placeholders:
841
+ reasons.append(f"IR contains {len(placeholders)} untranslated or unsupported placeholder nodes.")
842
+ if not meaningful:
843
+ reasons.append("IR contains no meaningful UI nodes.")
844
+ if design_mode == "reconstruct" and source_coverage < 0.8:
845
+ reasons.append(f"Only {source_coverage:.0%} of meaningful nodes map to source; at least 80% is required.")
846
+ if design_mode in {"generate", "redesign"} and evidence_coverage < 0.9:
847
+ reasons.append(f"Only {evidence_coverage:.0%} of meaningful nodes have project, standard, or decision evidence; at least 90% is required.")
848
+ if appearance_coverage < 0.9:
849
+ reasons.append(f"Only {appearance_coverage:.0%} of visual nodes have explicit appearance and geometry; at least 90% is required.")
850
+ if catalog.get("enforce") is True:
851
+ if catalog.get("status") != "ready":
852
+ reasons.append("The discovered project component catalog must be inspected and marked ready.")
853
+ if component_coverage < 1:
854
+ reasons.append("Every used project component must reference a mapped component catalog entry.")
855
+ if design_mode in {"generate", "redesign"}:
856
+ if not design.get("brief", {}).get("primaryTask"):
857
+ reasons.append("Generate and redesign modes require a brief.primaryTask.")
858
+ if ir.get("policyFile") and not design.get("policySnapshot"):
859
+ reasons.append("A discovered project UI policy must be copied into design.policySnapshot before design work.")
860
+ if missing_profiles or invalid_profiles:
861
+ reasons.append("Every target platform needs a valid Material 3, Apple HIG, web-platform, or reasoned project profile.")
862
+ if semantic_coverage < 1:
863
+ reasons.append("Every interactive node needs semantics.role and semantics.label.")
864
+ if standard_coverage < 1:
865
+ reasons.append("Every meaningful node needs a matching platform component reference or a valid design decision.")
866
+ if target_coverage < 1:
867
+ reasons.append(f"Interactive target sizes are missing or below platform baseline for {len(target_failures)} nodes.")
868
+ if contrast_coverage < 1 or contrast_failures:
869
+ reasons.append(f"Text contrast is unresolved for {len(contrast_unresolved)} nodes and below WCAG thresholds for {len(contrast_failures)} nodes.")
870
+ if missing_states or invalid_state_exclusions:
871
+ reasons.append("Every screen must cover its declared, task-relevant scenarios and default state; scenario exclusions need reasons.")
872
+ if design_mode in {"reconstruct", "redesign"} and missing_screen_keys:
873
+ reasons.append(f"IR is missing {len(missing_screen_keys)} discovered screens; translate or explicitly exclude them with reasons.")
874
+ if design_mode in {"reconstruct", "redesign"} and missing_routes:
875
+ reasons.append(f"IR is missing {len(missing_routes)} discovered routes; map or explicitly exclude them with reasons.")
876
+ if invalid_screen_exclusions or invalid_route_exclusions:
877
+ reasons.append("Every excluded screen or route must be an object with a stable key and non-empty reason.")
878
+ if design_mode in {"reconstruct", "redesign"} and missing_navigation_targets:
879
+ reasons.append(f"IR is missing screens or navigate actions for {len(missing_navigation_targets)} discovered navigation targets.")
880
+ if invalid_navigation_exclusions:
881
+ reasons.append("Every excluded navigation target must include target and a non-empty reason.")
882
+ return {
883
+ "status": "blocked" if reasons else "reviewable",
884
+ "designMode": design_mode,
885
+ "meaningfulNodes": len(meaningful),
886
+ "placeholderNodes": len(placeholders),
887
+ "sourceCoverage": source_coverage,
888
+ "evidenceCoverage": evidence_coverage,
889
+ "highConfidenceCoverage": confidence_coverage,
890
+ "appearanceCoverage": appearance_coverage,
891
+ "semanticCoverage": semantic_coverage,
892
+ "standardCoverage": standard_coverage,
893
+ "targetCoverage": target_coverage,
894
+ "contrastCoverage": contrast_coverage,
895
+ "stateCoverage": state_coverage,
896
+ "componentCoverage": component_coverage,
897
+ "visualFidelityTier": "structural" if native_evidence_required else "projection",
898
+ "nativeEvidenceRequired": native_evidence_required,
899
+ "nativeEvidenceCoverage": 0 if native_evidence_required else 1,
900
+ "nativeExecutionStarted": False,
901
+ "catalogComponents": len(catalog_entries),
902
+ "screenCoverage": round(len(expected_screen_keys & translated_screen_keys) / len(expected_screen_keys), 3) if expected_screen_keys else 1,
903
+ "screenAccountabilityCoverage": round((len(expected_screen_keys) - len(missing_screen_keys)) / len(expected_screen_keys), 3) if expected_screen_keys else 1,
904
+ "routeCoverage": round((len(expected_routes) - len(missing_routes)) / len(expected_routes), 3) if expected_routes else 1,
905
+ "interactionActions": len(actions),
906
+ "navigationActions": len(navigation_actions),
907
+ "navigationCoverage": round((len(expected_navigation_targets) - len(missing_navigation_targets)) / len(expected_navigation_targets), 3) if expected_navigation_targets else 1,
908
+ "missingNavigationTargets": missing_navigation_targets,
909
+ "missingScreens": missing_screen_keys,
910
+ "missingRoutes": missing_routes,
911
+ "targetFailures": target_failures,
912
+ "contrastFailures": contrast_failures,
913
+ "contrastUnresolved": contrast_unresolved,
914
+ "missingStates": missing_states,
915
+ "reasons": reasons,
916
+ }
917
+
918
+
919
+ def resolve_assets(ir: dict[str, Any]) -> dict[str, Any]:
920
+ rendered = copy.deepcopy(ir)
921
+ project_root = Path(rendered.get("project", {}).get("root", ".")).resolve()
922
+
923
+ def embed(asset: str) -> tuple[str | None, str | None]:
924
+ if asset.startswith("data:"):
925
+ return asset, None
926
+ if asset.startswith(("http://", "https://")):
927
+ return None, "Remote assets are not embedded"
928
+ path = (project_root / asset).resolve()
929
+ if not path.is_relative_to(project_root):
930
+ return None, "Asset path escapes the project root"
931
+ try:
932
+ if not path.is_file() or path.stat().st_size > 2_500_000:
933
+ return None, "Missing or larger than 2.5 MB"
934
+ mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
935
+ encoded = base64.b64encode(path.read_bytes()).decode("ascii")
936
+ return f"data:{mime};base64,{encoded}", None
937
+ except OSError as exc:
938
+ return None, str(exc)
939
+
940
+ for node in rendered.get("nodes", {}).values():
941
+ asset = node.get("asset")
942
+ if not isinstance(asset, str) or not asset:
943
+ continue
944
+ resolved, error = embed(asset)
945
+ if resolved:
946
+ node["resolvedAsset"] = resolved
947
+ if error:
948
+ node["assetError"] = error
949
+ for font in rendered.get("fonts", []):
950
+ asset = font.get("asset")
951
+ if not isinstance(asset, str) or not asset:
952
+ continue
953
+ resolved, error = embed(asset)
954
+ if resolved:
955
+ font["resolvedAsset"] = resolved
956
+ if error:
957
+ font["assetError"] = error
958
+ return rendered
959
+
960
+
961
+ CSS = r"""
962
+ *{box-sizing:border-box}html,body{margin:0;min-height:100%;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;background:#eef1f5;color:#111827}button,input,textarea,select{font:inherit}.review-app{min-height:100vh;display:grid;grid-template-columns:260px minmax(320px,1fr) 300px}.sidebar,.inspector{background:#fff}.sidebar{border-right:1px solid #d8dee8;padding:20px 14px}.inspector{border-left:1px solid #d8dee8;padding:20px;overflow-wrap:anywhere}.brand{font-size:15px;font-weight:700;margin:0 6px 4px}.project{font-size:12px;color:#667085;margin:0 6px 12px}.audit{margin:0 6px 18px;font-size:11px;color:#475467}.screen-list{display:grid;gap:5px}.screen-link{width:100%;border:0;background:transparent;color:#344054;text-align:left;padding:9px 10px;border-radius:8px;cursor:pointer}.screen-link:hover{background:#f2f4f7}.screen-link[aria-current=true]{background:#eaf1ff;color:#174ea6;font-weight:650}.workspace{min-width:0;display:flex;flex-direction:column}.toolbar{height:58px;background:#fff;border-bottom:1px solid #d8dee8;display:flex;align-items:center;justify-content:space-between;padding:0 18px;gap:14px}.toolbar-group{display:flex;align-items:center;gap:8px}.tool-button{border:1px solid #cfd6e1;background:#fff;color:#344054;border-radius:8px;padding:7px 11px;cursor:pointer}.tool-button[aria-pressed=true]{background:#1d4ed8;color:#fff;border-color:#1d4ed8}.screen-title{font-size:14px;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stage{flex:1;display:flex;align-items:flex-start;justify-content:center;padding:30px;overflow:auto;background-image:linear-gradient(45deg,rgba(15,23,42,.025) 25%,transparent 25%),linear-gradient(-45deg,rgba(15,23,42,.025) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(15,23,42,.025) 75%),linear-gradient(-45deg,transparent 75%,rgba(15,23,42,.025) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0}.device{box-sizing:content-box;flex:0 0 auto;position:relative;background:transparent;box-shadow:0 18px 55px rgba(15,23,42,.2);overflow:hidden;transform-origin:top center}.device.phone.framed{border:10px solid #20242b;border-radius:38px}.device.desktop.framed{border:1px solid #aeb7c5;border-radius:10px}.device-content{width:100%;height:100%;overflow:auto;position:relative;background:transparent}.device-content>.ui-node{min-height:100%}.ui-node{min-width:0;margin:0}.ui-node[data-selected=true]{outline:3px solid #2563eb!important;outline-offset:2px}.ui-container,.ui-card,.ui-list{display:flex}.ui-text{white-space:pre-wrap}.ui-button{appearance:none;-webkit-appearance:none;border:0;padding:0;margin:0;background:transparent;color:inherit;text-align:inherit;cursor:pointer}.ui-button:focus-visible,.tool-button:focus-visible,.screen-link:focus-visible{outline:3px solid #93c5fd;outline-offset:2px}.ui-input{appearance:none;-webkit-appearance:none;border:0;padding:0;margin:0;background:transparent;color:inherit;min-width:0}.ui-input::placeholder{color:var(--placeholder-color,currentColor);opacity:1}.ui-image{display:block}.ui-icon{display:inline-flex;align-items:center;justify-content:center}.ui-spacer{flex:0 0 auto}.ui-custom{padding:14px;border:1px dashed #d97706;border-radius:8px;background:#fff7ed;color:#9a3412;font-size:13px}.empty{padding:40px;text-align:center;color:#667085}.inspect-heading{font-size:13px;margin:0 0 14px}.inspect-empty{font-size:13px;line-height:1.5;color:#667085}.inspect-table{display:grid;grid-template-columns:86px 1fr;gap:8px 10px;font-size:12px}.inspect-table dt{color:#667085}.inspect-table dd{margin:0;font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.confidence{display:inline-flex;padding:2px 7px;border-radius:999px;font-size:11px;font-family:inherit}.confidence.exact{background:#dcfce7;color:#166534}.confidence.high{background:#dbeafe;color:#1e40af}.confidence.approximate{background:#ffedd5;color:#9a3412}.confidence.unsupported{background:#fee2e2;color:#991b1b}.warning-panel{margin-top:22px;border-top:1px solid #eaecf0;padding-top:16px}.warning-panel summary{cursor:pointer;font-size:12px;font-weight:650}.warning-panel ul{padding-left:18px;font-size:12px;line-height:1.45;color:#667085}@media(max-width:980px){.review-app{grid-template-columns:210px minmax(300px,1fr)}.inspector{display:none}}@media(max-width:720px){.review-app{display:block}.sidebar{border:0;border-bottom:1px solid #d8dee8}.screen-list{display:flex;overflow:auto}.screen-link{width:auto;white-space:nowrap}.toolbar{position:sticky;top:0;z-index:5}.stage{padding:18px}}
963
+ """
964
+
965
+ VIEW_CSS = r"""
966
+ .toolbar-controls{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.view-button{border:0;background:transparent;color:#475467;border-radius:8px;padding:7px 10px;cursor:pointer}.view-button[aria-pressed=true]{background:#eaf1ff;color:#174ea6;font-weight:650}.stage.overview{display:block;padding:28px}.overview-canvas-shell{position:relative;min-width:100%;min-height:100%}.overview-canvas{position:absolute;left:0;top:0;transform-origin:top left}.overview-grid{display:flex;flex-wrap:wrap;gap:24px;align-items:flex-start;align-content:flex-start}.screen-card{flex:0 0 auto;min-width:0}.screen-card-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}.screen-card-title{font-size:13px;font-weight:700;color:#344054;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.screen-card-open{border:1px solid #cfd6e1;background:#fff;color:#344054;border-radius:7px;padding:5px 8px;cursor:pointer;font-size:11px}.overview-viewport{position:relative;overflow:hidden;background:transparent;box-shadow:0 8px 24px rgba(15,23,42,.14)}.overview-device{position:absolute;left:0;top:0;transform-origin:top left;background:transparent;overflow:hidden}.overview-device .device-content{overflow:hidden}.stage.prototype .device,.stage.single .device{flex:0 0 auto}.stage.overview .ui-node{pointer-events:none}.stage.overview.inspect-mode .ui-node{pointer-events:auto}.view-button:focus-visible,.screen-card-open:focus-visible{outline:3px solid #93c5fd;outline-offset:2px}@media(max-width:1100px){.toolbar{height:auto;min-height:58px;padding-top:8px;padding-bottom:8px}.toolbar-controls{gap:8px}}@media(max-width:720px){.stage.overview{padding:18px}}
967
+ """
968
+
969
+
970
+ LEGACY_JS = r"""
971
+ const ir=JSON.parse(document.getElementById('ui-ir-data').textContent);const nodes=ir.nodes||{};const screens=ir.screens||[];const state={interaction:'interact',view:'overview',screen:null,selected:null,nodeStates:{}};
972
+ const $=s=>document.querySelector(s);const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));const iconMarkup=name=>`<svg class="icon" aria-hidden="true"><use href="#icon-${esc(name)}"></use></svg>`;
973
+ function token(path){let current=path,android=false;const seen=new Set();while(typeof current==='string'&&current.startsWith('$')&&!seen.has(current)){seen.add(current);let value=ir.tokens;for(const part of current.slice(1).split('.'))value=value?.[part];if(value==null)return current;if(value&&typeof value==='object'){android=android||value.adapter==='android-xml';current=Object.prototype.hasOwnProperty.call(value,'value')?value.value:value}else current=value}if(android&&typeof current==='string'&&/^#[0-9a-f]{8}$/i.test(current))current=`#${current.slice(3)}${current.slice(1,3)}`;return current}
974
+ function cssValue(value,unit='px'){value=token(value);if(typeof value==='number')return `${value}${unit}`;if(typeof value==='string'&&/^-?\d+(?:\.\d+)?(?:dp|sp|pt)$/.test(value))return `${parseFloat(value)}px`;if(value==='fill')return '100%';if(value==='hug')return 'fit-content';return value??''}
975
+ function styleFor(node){
976
+ const l=node.layout||{},s=node.style||{},out={};
977
+ const raw=(key,value)=>{if(value!=null)out[key]=token(value)};
978
+ const length=(key,value)=>{if(value!=null)out[key]=cssValue(value)};
979
+ const direction=l.direction||((node.type==='container'||node.type==='list'||node.type==='card')?'column':null);
980
+ if(direction){
981
+ out.display='flex';
982
+ if(direction==='row'||direction==='column')out.flexDirection=direction;
983
+ if(direction==='grid'){
984
+ out.display='grid';
985
+ const columns=l.columns??2;
986
+ out.gridTemplateColumns=typeof columns==='number'?`repeat(${columns},minmax(0,1fr))`:token(columns);
987
+ if(l.rows!=null)out.gridTemplateRows=token(l.rows);
988
+ }
989
+ if(direction==='overlay'){out.position='relative';out.display='grid'}
990
+ }
991
+ length('gap',l.gap);length('rowGap',l.rowGap);length('columnGap',l.columnGap);
992
+ length('padding',l.padding);length('margin',l.margin);
993
+ if(l.paddingHorizontal!=null){length('paddingLeft',l.paddingHorizontal);length('paddingRight',l.paddingHorizontal)}
994
+ if(l.paddingVertical!=null){length('paddingTop',l.paddingVertical);length('paddingBottom',l.paddingVertical)}
995
+ if(l.marginHorizontal!=null){length('marginLeft',l.marginHorizontal);length('marginRight',l.marginHorizontal)}
996
+ if(l.marginVertical!=null){length('marginTop',l.marginVertical);length('marginBottom',l.marginVertical)}
997
+ for(const side of ['Top','Right','Bottom','Left']){
998
+ length(`padding${side}`,l[`padding${side}`]);length(`margin${side}`,l[`margin${side}`]);
999
+ }
1000
+ for(const key of ['width','height','minWidth','maxWidth','minHeight','maxHeight','top','right','bottom','left','flexBasis'])length(key,l[key]);
1001
+ raw('position',l.position);raw('display',l.display);raw('overflow',l.overflow);raw('overflowX',l.overflowX);raw('overflowY',l.overflowY);
1002
+ raw('order',l.order);raw('zIndex',l.zIndex);raw('aspectRatio',l.aspectRatio);raw('gridArea',l.gridArea);
1003
+ raw('flexGrow',l.grow);raw('flexShrink',l.shrink);raw('flexWrap',l.wrap===true?'wrap':l.wrap);
1004
+ if(l.align)out.alignItems=({start:'flex-start',end:'flex-end',center:'center',stretch:'stretch',baseline:'baseline'}[l.align]||l.align);
1005
+ if(l.justify)out.justifyContent=({start:'flex-start',end:'flex-end',between:'space-between',around:'space-around',evenly:'space-evenly',center:'center'}[l.justify]||l.justify);
1006
+ if(l.alignSelf)out.alignSelf=({start:'flex-start',end:'flex-end',center:'center',stretch:'stretch',baseline:'baseline'}[l.alignSelf]||l.alignSelf);
1007
+ if(l.justifySelf)out.justifySelf=({start:'start',end:'end',center:'center',stretch:'stretch'}[l.justifySelf]||l.justifySelf);
1008
+ const lengths={borderWidth:'borderWidth',borderTopWidth:'borderTopWidth',borderRightWidth:'borderRightWidth',borderBottomWidth:'borderBottomWidth',borderLeftWidth:'borderLeftWidth',radius:'borderRadius',radiusTopLeft:'borderTopLeftRadius',radiusTopRight:'borderTopRightRadius',radiusBottomRight:'borderBottomRightRadius',radiusBottomLeft:'borderBottomLeftRadius',fontSize:'fontSize',lineHeight:'lineHeight',letterSpacing:'letterSpacing',outlineWidth:'outlineWidth'};
1009
+ const raws={background:'background',backgroundColor:'backgroundColor',backgroundImage:'backgroundImage',color:'color',border:'border',borderColor:'borderColor',borderTopColor:'borderTopColor',borderRightColor:'borderRightColor',borderBottomColor:'borderBottomColor',borderLeftColor:'borderLeftColor',borderStyle:'borderStyle',outlineColor:'outlineColor',outlineStyle:'outlineStyle',opacity:'opacity',fontFamily:'fontFamily',fontWeight:'fontWeight',fontStyle:'fontStyle',textAlign:'textAlign',textDecoration:'textDecoration',textTransform:'textTransform',whiteSpace:'whiteSpace',overflowWrap:'overflowWrap',textOverflow:'textOverflow',objectFit:'objectFit',objectPosition:'objectPosition',boxShadow:'boxShadow',filter:'filter',backdropFilter:'backdropFilter',transform:'transform',transformOrigin:'transformOrigin',cursor:'cursor',visibility:'visibility'};
1010
+ for(const [key,cssKey] of Object.entries(lengths))length(cssKey,s[key]);
1011
+ for(const [key,cssKey] of Object.entries(raws))raw(cssKey,s[key]);
1012
+ if(s.shadow!=null)raw('boxShadow',s.shadow);
1013
+ if(s.placeholderColor!=null)raw('--placeholder-color',s.placeholderColor);
1014
+ const typo=token(s.typography);
1015
+ if(typo&&typeof typo==='object'){
1016
+ for(const key of ['fontSize','lineHeight','letterSpacing'])length(key,typo[key]);
1017
+ for(const key of ['fontFamily','fontWeight','fontStyle','textTransform'])raw(key,typo[key]);
1018
+ }
1019
+ if(s.css&&typeof s.css==='object')for(const [key,value] of Object.entries(s.css)){
1020
+ if(/^(--[a-z0-9-]+|[a-zA-Z][a-zA-Z0-9-]*)$/.test(key))out[key]=token(value);
1021
+ }
1022
+ return Object.entries(out).filter(([,v])=>v!==''&&v!=null).map(([k,v])=>`${k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase())}:${v}`).join(';')
1023
+ }
1024
+ function effectiveNode(id){const base=nodes[id];if(!base)return null;const variant=base.states?.[state.nodeStates[id]];if(!variant)return base;return {...base,...variant,layout:{...(base.layout||{}),...(variant.layout||{})},style:{...(base.style||{}),...(variant.style||{})}}}
1025
+ function provenance(id,node){const src=node.source||{},action=node.action||{},semantics=node.semantics||{},standard=node.standardRef||Object.entries(node.standardRefs||{}).map(([platform,ref])=>`${platform}:${ref}`).join(', '),actionType=String(action.type||'').trim(),actionAttrs=actionType?` data-action="${esc(actionType)}" data-target="${esc(action.target||'')}" data-action-state="${esc(action.state||'')}" data-action-off-state="${esc(action.offState||'')}"`:'';return `data-node-id="${esc(id)}" data-source-file="${esc(src.file||'')}" data-source-line="${esc(src.line||'')}" data-source-symbol="${esc(src.symbol||'')}" data-component="${esc(node.component||'')}" data-confidence="${esc(node.confidence||'approximate')}" data-standard-ref="${esc(standard)}" data-decision-id="${esc(node.decisionId||'')}" data-semantic-role="${esc(semantics.role||'')}" data-semantic-label="${esc(semantics.label||'')}" data-target-size="${esc(typeof semantics.targetSize==='object'?`${semantics.targetSize.width||'?'}×${semantics.targetSize.height||'?'}`:(semantics.targetSize||''))}"${actionAttrs}`}
1026
+ function renderNode(id,stack=[]){const n=effectiveNode(id);if(!n)return `<div class="ui-custom">Missing node ${esc(id)}</div>`;if(stack.includes(id))return `<div class="ui-custom">Circular node ${esc(id)}</div>`;const style=styleFor(n),p=provenance(id,n),children=(n.children||[]).map(child=>renderNode(child,[...stack,id])).join('');switch(n.type){case'text':return `<div class="ui-node ui-text" ${p} style="${esc(style)}">${esc(n.text||'')}</div>`;case'button':return `<button type="button" class="ui-node ui-button" ${p} ${n.disabled?'disabled':''} style="${esc(style)}">${children||esc(n.text||n.component||'')}</button>`;case'input':return `<input class="ui-node ui-input" ${p} type="${esc(n.inputType||'text')}" value="${esc(n.value||'')}" placeholder="${esc(n.placeholder||'')}" ${n.disabled?'disabled':''} style="${esc(style)}">`;case'image':return n.resolvedAsset?`<img class="ui-node ui-image" ${p} src="${n.resolvedAsset}" alt="${esc(n.alt||'')}" style="${esc(style)}">`:`<div class="ui-node ui-custom" ${p}>Asset unavailable: ${esc(n.asset||'')}</div>`;case'icon':return n.resolvedAsset?`<img class="ui-node ui-icon" ${p} src="${n.resolvedAsset}" alt="${esc(n.alt||'')}" style="${esc(style)}">`:`<span class="ui-node ui-icon" ${p} style="${esc(style)}">${esc(n.iconName||'◇')}</span>`;case'spacer':return `<div class="ui-node ui-spacer" ${p} style="${esc(style)}"></div>`;case'custom':return `<div class="ui-node ui-custom" ${p} style="${esc(style)}">${esc(n.text||n.component||'Unsupported UI')}</div>`;case'card':return `<div class="ui-node ui-card" ${p} style="${esc(style)}">${children}</div>`;case'list':return `<div class="ui-node ui-list" ${p} style="${esc(style)}">${children}</div>`;default:return `<div class="ui-node ui-container" ${p} style="${esc(style)}">${children}</div>`}}
1027
+ const vp=ir.viewport||{width:390,height:844,device:'phone'};
1028
+ function currentFromLocation(){const id=new URL(location.href).searchParams.get('screen');return screens.some(s=>s.id===id)?id:screens[0]?.id}
1029
+ function viewFromLocation(){const view=new URL(location.href).searchParams.get('view');return ['overview','prototype','single'].includes(view)?view:'overview'}
1030
+ function viewportFor(screen){return {...vp,...(screen?.viewport||{})}}
1031
+ function writeLocation(push=true){const u=new URL(location.href);u.searchParams.set('view',state.view);if(state.screen)u.searchParams.set('screen',state.screen);const payload={view:state.view,screen:state.screen};push?history.pushState(payload,'',u):history.replaceState(payload,'',u)}
1032
+ function deviceMarkup(screen,extraClass=''){const screenVp=viewportFor(screen);return `<div class="device ${esc(extraClass)}" data-width="${esc(screenVp.width)}" data-height="${esc(screenVp.height)}" data-device="${esc(screenVp.device||'phone')}" data-frame="${screenVp.frame===true}" data-background="${esc(screenVp.background||'')}"><div class="device-content">${renderNode(screen.root)}</div></div>`}
1033
+ function configureDevice(device){device.style.width=`${device.dataset.width}px`;device.style.height=`${device.dataset.height}px`;device.classList.add(device.dataset.device==='desktop'?'desktop':'phone');if(device.dataset.frame==='true')device.classList.add('framed');device.querySelector('.device-content').style.background=token(device.dataset.background)||'transparent'}
1034
+ function overviewMarkup(){return `<div class="overview-grid">${screens.map(screen=>{const screenVp=viewportFor(screen),scale=screenVp.device==='desktop'?0.2:0.52;return `<article class="screen-card" data-screen-card="${esc(screen.id)}"><header class="screen-card-header"><span class="screen-card-title">${esc(screen.name)}</span><button type="button" class="screen-card-open" data-open-screen="${esc(screen.id)}">Open</button></header><div class="overview-viewport" style="width:${screenVp.width*scale}px;height:${screenVp.height*scale}px">${deviceMarkup(screen,'overview-device').replace('class="device overview-device"',`class="device overview-device" style="transform:scale(${scale})"`)}</div></article>`}).join('')}</div>`}
1035
+ function renderView(){const screen=screens.find(item=>item.id===state.screen)||screens[0];state.screen=screen?.id;document.querySelectorAll('[data-view]').forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.view===state.view)));document.querySelectorAll('.screen-link').forEach(button=>button.setAttribute('aria-current',String(button.dataset.screen===state.screen)));$('.screen-title').textContent=state.view==='overview'?`All screens (${screens.length})`:screen?.name||'';const stage=$('.stage');stage.className=`stage ${state.view}${state.interaction==='inspect'?' inspect-mode':''}`;stage.innerHTML=state.view==='overview'?overviewMarkup():(screen?deviceMarkup(screen):'<div class="empty">No screens</div>');document.querySelectorAll('.device').forEach(configureDevice);bindNodes();document.querySelectorAll('[data-open-screen]').forEach(button=>button.addEventListener('click',()=>setScreen(button.dataset.openScreen,'single')));selectNode(null)}
1036
+ function setView(view,push=true){if(!['overview','prototype','single'].includes(view))return;state.view=view;writeLocation(push);renderView()}
1037
+ function setScreen(id,view='single',push=true){if(!screens.some(screen=>screen.id===id))return;state.screen=id;state.view=view;writeLocation(push);renderView()}
1038
+ function navigate(id,push=true){setScreen(id,'prototype',push)}
1039
+ function applyAction(el,event){const type=el.dataset.action,target=el.dataset.target;if(!type||state.view==='overview')return;if(type==='navigate'){if(state.view==='prototype'){event.preventDefault();event.stopPropagation();navigate(target)}return}if(type==='back'){event.preventDefault();event.stopPropagation();history.back();return}if(type==='set-node-state'){event.preventDefault();event.stopPropagation();state.nodeStates[target]=el.dataset.actionState;renderView();return}if(type==='toggle-node-state'||type==='toggle'){event.preventDefault();event.stopPropagation();state.nodeStates[target]=state.nodeStates[target]===el.dataset.actionState?(el.dataset.actionOffState||'default'):el.dataset.actionState;renderView();return}if(type==='reset-state'){event.preventDefault();event.stopPropagation();state.nodeStates={};renderView()}}
1040
+ function bindNodes(){document.querySelectorAll('.ui-node').forEach(el=>el.addEventListener('click',event=>{if(state.interaction==='inspect'){event.preventDefault();event.stopPropagation();selectNode(el);return}applyAction(el,event)}))}
1041
+ function selectNode(el){document.querySelectorAll('[data-selected=true]').forEach(n=>n.removeAttribute('data-selected'));state.selected=el;if(!el){$('.inspect-body').innerHTML='<p class="inspect-empty">Switch to Inspect and select an element to see its source mapping, standard, semantics, and fidelity.</p>';return}el.dataset.selected='true';const c=el.dataset.confidence||'approximate';$('.inspect-body').innerHTML=`<dl class="inspect-table"><dt>Node</dt><dd>${esc(el.dataset.nodeId)}</dd><dt>Component</dt><dd>${esc(el.dataset.component||'—')}</dd><dt>Source</dt><dd>${esc(el.dataset.sourceFile||'—')}${el.dataset.sourceLine?':'+esc(el.dataset.sourceLine):''}</dd><dt>Symbol</dt><dd>${esc(el.dataset.sourceSymbol||'—')}</dd><dt>Standard</dt><dd>${esc(el.dataset.standardRef||'—')}</dd><dt>Decision</dt><dd>${esc(el.dataset.decisionId||'—')}</dd><dt>Semantics</dt><dd>${esc(el.dataset.semanticRole||'—')}${el.dataset.semanticLabel?' · '+esc(el.dataset.semanticLabel):''}</dd><dt>Target</dt><dd>${esc(el.dataset.targetSize||'—')}</dd><dt>Fidelity</dt><dd><span class="confidence ${esc(c)}">${esc(c)}</span></dd></dl>`}
1042
+ document.querySelectorAll('[data-interaction]').forEach(button=>button.addEventListener('click',()=>{state.interaction=button.dataset.interaction;document.querySelectorAll('[data-interaction]').forEach(item=>item.setAttribute('aria-pressed',String(item===button)));renderView()}));document.querySelectorAll('[data-view]').forEach(button=>button.addEventListener('click',()=>setView(button.dataset.view)));document.querySelectorAll('.screen-link').forEach(button=>button.addEventListener('click',()=>setScreen(button.dataset.screen,'single')));addEventListener('popstate',()=>{state.screen=currentFromLocation();state.view=viewFromLocation();renderView()});state.screen=currentFromLocation();state.view=viewFromLocation();writeLocation(false);renderView();
1043
+ """
1044
+
1045
+ REVIEW_CSS = r"""
1046
+ .review-app{--left-panel:230px;--right-panel:300px;--left-grip:5px;--right-grip:5px;height:100vh;min-height:0;overflow:hidden;grid-template-columns:var(--left-panel) var(--left-grip) minmax(360px,1fr) var(--right-grip) var(--right-panel)}
1047
+ .sidebar,.inspector{min-width:0;height:100vh;overflow:auto;padding:12px 10px}.brand{font-size:13px;margin:0 4px 2px}.project{font-size:10px;margin:0 4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.audit-panel{margin:0 2px 8px;border:1px solid #e4e7ec;border-radius:6px;background:#f8fafc}.audit-panel>summary{cursor:pointer;list-style:none;padding:5px 7px;font-size:10px;font-weight:650;color:#475467}.audit-panel>summary::-webkit-details-marker{display:none}.audit-panel>summary:before{content:'›';display:inline-block;margin-right:5px}.audit-panel[open]>summary:before{transform:rotate(90deg)}.audit-panel .audit{margin:0;padding:0 7px 7px;font-size:9px;line-height:1.35;color:#667085}
1048
+ .panel-resizer{position:relative;z-index:20;background:#e4e7ec;cursor:col-resize;touch-action:none}.panel-resizer:after{content:'';position:absolute;inset:0 -3px}.panel-resizer:hover,.panel-resizer:focus-visible,.review-app.resizing .panel-resizer{background:#60a5fa;outline:none}.review-app.sidebar-hidden>.sidebar,.review-app.inspector-hidden>.inspector{visibility:hidden;padding:0;border:0;overflow:hidden}.review-app.sidebar-hidden>.left-resizer,.review-app.inspector-hidden>.right-resizer{pointer-events:none}
1049
+ .workspace{height:100vh;min-height:0;position:relative;overflow:hidden;container-type:inline-size}.workbench-header{flex:0 0 auto;position:relative;z-index:15;box-shadow:0 2px 8px rgba(15,23,42,.16)}.menu-bar{height:29px;display:flex;align-items:center;gap:10px;padding:0 8px;background:#f8fafc;border-bottom:1px solid #cbd5e1;color:#334155}.app-mark{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:750;white-space:nowrap}.app-mark-dot{width:8px;height:8px;border-radius:2px;background:#2563eb}.menu-strip{display:flex;align-items:stretch;height:100%}.menu{position:relative;margin:0}.menu>summary{height:100%;display:flex;align-items:center;list-style:none;padding:0 7px;cursor:pointer;font-size:10px;color:#475569}.menu>summary::-webkit-details-marker{display:none}.menu>summary:hover,.menu[open]>summary{background:#e2e8f0;color:#0f172a}.menu-popover{position:absolute;top:27px;left:0;z-index:30;min-width:176px;padding:5px;background:#fff;border:1px solid #cbd5e1;border-radius:7px;box-shadow:0 10px 28px rgba(15,23,42,.18)}.menu-popover button{display:block;width:100%;border:0;border-radius:5px;background:transparent;padding:6px 8px;text-align:left;color:#334155;font-size:11px;cursor:pointer}.menu-popover button:hover{background:#eff6ff;color:#1d4ed8}.menu-spacer{flex:1}.version-select{max-width:180px;border:1px solid #cbd5e1;border-radius:5px;padding:3px 6px;background:#fff;color:#334155;font-size:10px}
1050
+ .action-bar{height:46px;display:flex;align-items:center;gap:8px;padding:6px 8px;background:#18324f;color:#fff}.panel-toggle{flex:0 0 auto;width:28px;height:28px;display:grid;place-items:center;border:1px solid rgba(255,255,255,.22);border-radius:6px;background:rgba(255,255,255,.08);color:#fff;font-size:18px;line-height:1;cursor:pointer}.panel-toggle:hover{background:rgba(255,255,255,.18)}.screen-context{min-width:110px;max-width:240px;display:grid;gap:1px}.screen-title{font-size:11px;color:#fff}.navigation-preview-label{display:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px;color:#fbbf24}.navigation-preview-label.visible{display:block}.mode-switch,.interaction-switch{display:flex;align-items:center;padding:2px;border-radius:7px;background:rgba(3,16,31,.34)}.mode-button,.interaction-button,.compare-button{height:28px;border:0;border-radius:5px;padding:0 8px;background:transparent;color:#cbd5e1;font-size:10px;white-space:nowrap;cursor:pointer}.mode-button .button-icon,.interaction-button .button-icon{margin-right:4px;font-size:12px}.mode-button[aria-pressed=true],.interaction-button[aria-pressed=true]{background:#fff;color:#174ea6;font-weight:700;box-shadow:0 1px 3px rgba(0,0,0,.18)}.mode-status{display:flex;align-items:center;gap:5px;min-width:0;padding:4px 7px;border:1px solid rgba(255,255,255,.2);border-radius:999px;color:#dbeafe;font-size:9px;white-space:nowrap}.mode-status:before{content:'';width:6px;height:6px;border-radius:50%;background:#94a3b8}.mode-status.prototype:before{background:#22c55e;box-shadow:0 0 0 3px rgba(34,197,94,.18)}.action-spacer{flex:1}.compare-controls{display:none;align-items:center;gap:2px}.review-app[data-view=compare] .compare-controls{display:flex}.review-app[data-view=compare] .screen-context{display:none}.review-app[data-view=compare] .interaction-button .button-label{display:none}.review-app[data-view=compare] .interaction-button{width:30px;padding:0}.compare-button{background:rgba(255,255,255,.08)}.compare-button[aria-pressed=true]{background:#fff;color:#174ea6}
1051
+ .stage{position:relative;min-height:0}.stage.canvas-panning{cursor:grabbing;user-select:none}.stage.canvas-panning *{cursor:grabbing!important}.stage.prototype,.stage.single{justify-content:flex-start}.stage.prototype>.device,.stage.single>.device{margin-inline:auto}.floating-zoom{position:absolute;right:14px;bottom:14px;z-index:16;display:flex;align-items:center;gap:2px;padding:3px;border:1px solid #cbd5e1;border-radius:8px;background:rgba(255,255,255,.94);box-shadow:0 5px 18px rgba(15,23,42,.18);backdrop-filter:blur(6px)}.zoom-button{height:25px;min-width:25px;border:0;border-radius:5px;background:transparent;color:#334155;font-size:12px;cursor:pointer}.zoom-button:hover{background:#eaf1ff;color:#174ea6}.zoom-label{min-width:39px;text-align:center;font-size:9px;color:#475467}.workbench-toast{position:absolute;left:50%;bottom:18px;z-index:40;transform:translate(-50%,12px);max-width:min(440px,calc(100% - 120px));padding:8px 11px;border-radius:7px;background:#0f172a;color:#fff;font-size:11px;box-shadow:0 10px 30px rgba(15,23,42,.25);opacity:0;pointer-events:none;transition:.18s}.workbench-toast.visible{opacity:1;transform:translate(-50%,0)}
1052
+ .screen-list{display:block}.screen-tree-group{margin:1px 0}.screen-tree-group>summary{list-style:none;display:flex;align-items:center;gap:5px;min-height:24px;padding:3px 6px;border-radius:5px;cursor:pointer;color:#475467;font-size:10px;font-weight:700;line-height:1.2}.screen-tree-group>summary::-webkit-details-marker{display:none}.screen-tree-group>summary:before{content:'›';display:inline-block;transition:transform .15s}.screen-tree-group[open]>summary:before{transform:rotate(90deg)}.screen-tree-group>summary:hover{background:#f2f4f7}.screen-tree-children{margin-left:7px;padding-left:5px;border-left:1px solid #e4e7ec}.screen-link{position:relative;min-height:24px;padding-top:4px!important;padding-bottom:4px!important;padding-right:6px!important;border-radius:5px;font-size:11px;line-height:1.25;white-space:normal}.screen-link[aria-current=true]{background:#eaf1ff;color:#174ea6;font-weight:700}.screen-link[aria-current=true]:before{content:'';position:absolute;left:1px;top:5px;bottom:5px;width:2px;border-radius:2px;background:#2563eb}.screen-link[data-navigation-preview=true]{background:#fff4e5;color:#b54708;box-shadow:inset 0 0 0 1px #fdb022;font-weight:700}.screen-link[aria-current=true][data-navigation-preview=true]{background:linear-gradient(90deg,#eaf1ff 0 72%,#fff4e5 72%);color:#174ea6;box-shadow:inset -3px 0 0 #f79009}.tree-count{margin-left:auto;padding:1px 4px;border-radius:999px;background:#eef2f6;color:#64748b;font-size:8px;font-weight:600}
1053
+ .inspector-header{display:flex;align-items:center;justify-content:space-between;gap:8px}.inspect-heading{margin:0;font-size:12px}.inspector-help{margin:8px 0 10px;border:1px solid #dbe3ee;border-radius:7px;background:#f8fafc}.inspector-help>summary{padding:6px 8px;cursor:pointer;font-size:10px;font-weight:700;color:#475569}.inspector-help p{margin:0;padding:0 8px 8px;color:#64748b;font-size:10px;line-height:1.45}.review-actions{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin:10px 0}.review-action{border:1px solid #cfd6e1;background:#fff;color:#344054;border-radius:6px;padding:6px 7px;cursor:pointer;font-size:10px}.review-action.primary{background:#1d4ed8;border-color:#1d4ed8;color:#fff}.review-decision{margin:7px 0 12px;padding:7px;border-radius:6px;background:#f2f4f7;color:#475467;font-size:10px}.review-decision.accepted{background:#dcfce7;color:#166534}.review-decision.rejected{background:#fee2e2;color:#991b1b}.queue-heading{font-size:11px;margin:15px 0 8px;padding-top:12px;border-top:1px solid #eaecf0}.annotation-list{display:grid;gap:7px}.annotation-card{border:1px solid #d8dee8;border-radius:7px;padding:8px;background:#fff}.annotation-card[data-status=accepted],.annotation-card[data-status=resolved]{border-color:#86efac}.annotation-card[data-status=rejected]{border-color:#fca5a5}.annotation-meta{display:flex;justify-content:space-between;gap:8px;font-size:9px;color:#667085;text-transform:uppercase}.annotation-text{font-size:11px;line-height:1.4;margin:6px 0;color:#344054}.annotation-target{border:0;background:transparent;color:#1d4ed8;padding:0;cursor:pointer;font-size:10px;text-align:left}.annotation-controls{display:flex;gap:4px;flex-wrap:wrap;margin-top:7px}.annotation-status{border:1px solid #cfd6e1;background:#fff;border-radius:5px;padding:3px 5px;font-size:9px;cursor:pointer}.comment-form{display:grid;gap:7px;margin-top:10px;padding-top:10px;border-top:1px solid #eaecf0}.comment-form textarea{width:100%;min-height:72px;resize:vertical;border:1px solid #cfd6e1;border-radius:6px;padding:8px;font-size:11px}.comment-form select{border:1px solid #cfd6e1;border-radius:6px;padding:5px;font-size:10px}.comment-form button{border:0;border-radius:6px;background:#1d4ed8;color:#fff;padding:7px;cursor:pointer;font-size:10px}.inspect-empty{font-size:11px}.inspect-table{grid-template-columns:72px 1fr;gap:6px 8px;font-size:10px}
1054
+ .automated-review{margin-top:10px;padding-top:10px;border-top:1px solid #e2e8f0}.review-section-heading{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:7px}.review-section-heading h2{margin:0;font-size:11px}.finding-total{padding:2px 6px;border-radius:999px;background:#eaf1ff;color:#174ea6;font-size:9px;font-weight:700}.audit-summary{margin-bottom:7px;color:#475569;font-size:10px;line-height:1.4}.audit-counts{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}.audit-count{padding:2px 5px;border-radius:999px;background:#f1f5f9;color:#475569;font-size:8px}.audit-gaps{margin-top:6px}.audit-gaps summary{cursor:pointer;font-size:9px;font-weight:700;color:#64748b}.audit-gaps ul{margin:5px 0 0;padding-left:16px;font-size:9px;color:#64748b}.finding-filters{display:flex;gap:3px;overflow-x:auto;margin:7px 0}.finding-filter{border:1px solid #d8dee8;border-radius:5px;background:#fff;padding:3px 5px;color:#475569;font-size:8px;cursor:pointer;white-space:nowrap}.finding-filter[aria-pressed=true]{border-color:#2563eb;background:#eaf1ff;color:#174ea6;font-weight:700}.finding-list{display:grid;gap:6px}.finding-card{border:1px solid #d8dee8;border-left-width:4px;border-radius:7px;padding:7px;background:#fff}.finding-card[data-severity=blocker]{border-left-color:#7f1d1d}.finding-card[data-severity=high]{border-left-color:#dc2626}.finding-card[data-severity=medium]{border-left-color:#f59e0b}.finding-card[data-severity=low]{border-left-color:#3b82f6}.finding-card[data-decision=accepted]{background:#f0fdf4}.finding-card[data-decision=rejected]{opacity:.62}.finding-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:6px}.finding-title{margin:0;color:#1e293b;font-size:10px;line-height:1.3}.finding-severity{flex:0 0 auto;padding:2px 4px;border-radius:4px;background:#f1f5f9;color:#475569;font-size:7px;font-weight:800;text-transform:uppercase}.finding-meta{margin-top:3px;color:#64748b;font-size:8px}.finding-observation,.finding-impact,.finding-recommendation{margin:5px 0 0;color:#475569;font-size:9px;line-height:1.4}.finding-recommendation{color:#1e3a5f}.finding-evidence{margin-top:5px}.finding-evidence summary{cursor:pointer;color:#475569;font-size:8px;font-weight:700}.finding-evidence ul{margin:4px 0 0;padding-left:14px;color:#64748b;font-size:8px;line-height:1.35}.finding-actions{display:flex;gap:3px;flex-wrap:wrap;margin-top:6px}.finding-action{border:1px solid #cbd5e1;border-radius:5px;background:#fff;padding:3px 5px;color:#334155;font-size:8px;cursor:pointer}.finding-action.primary{border-color:#2563eb;background:#2563eb;color:#fff}.finding-action[aria-pressed=true]{border-color:#16a34a;background:#dcfce7;color:#166534}.screen-issue-count{margin-left:auto;min-width:15px;padding:1px 4px;border-radius:999px;background:#fee2e2;color:#b91c1c;text-align:center;font-size:8px;font-weight:800}.screen-issue-count[data-severity=medium],.screen-issue-count[data-severity=low]{background:#fff4e5;color:#b54708}.ui-node[data-finding-severity=blocker],.ui-node[data-finding-severity=high]{outline:3px solid #dc2626!important;outline-offset:2px}.ui-node[data-finding-severity=medium]{outline:3px solid #f59e0b!important;outline-offset:2px}.ui-node[data-finding-severity=low]{outline:3px solid #3b82f6!important;outline-offset:2px}.panel-section{margin:9px 0;border:1px solid #e2e8f0;border-radius:7px}.panel-section>summary{padding:6px 8px;cursor:pointer;color:#475569;font-size:10px;font-weight:700}.panel-section>.inspect-body{padding:0 8px 8px}
1055
+ .compare-grid{display:grid;grid-template-columns:repeat(2,minmax(max-content,1fr));gap:28px;align-items:start}.compare-panel{display:grid;justify-items:center;gap:9px}.compare-label{font-size:12px;font-weight:700;color:#344054}.compare-overlay{position:relative;overflow:hidden;box-shadow:0 18px 55px rgba(15,23,42,.2)}.compare-overlay .device{position:absolute;inset:0;box-shadow:none}.compare-overlay .after-layer{overflow:hidden;width:var(--overlay-width,50%)}.compare-overlay-slider{width:min(520px,100%)}.ui-node[data-changed=true]{outline:2px dashed #7c3aed;outline-offset:-2px}.stage.compare{display:block}.stage.compare .compare-grid,.stage.compare .compare-overlay-wrap{width:max-content;margin:auto}.stage.compare .ui-node{pointer-events:none}.stage.comment-mode .ui-node{cursor:crosshair}.stage.comment-mode .ui-node:hover{outline:2px solid #f59e0b;outline-offset:1px}.status-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#98a2b3;margin-right:5px}.status-dot.proposal{background:#f59e0b}.status-dot.approved{background:#22c55e}.status-dot.rejected{background:#ef4444}
1056
+ .audit-depth{margin-top:6px;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc}.audit-depth>summary{padding:5px 6px;cursor:pointer;font-size:9px;font-weight:750;color:#475569}.audit-depth-list{display:grid;gap:4px;margin:0;padding:0 6px 6px;list-style:none}.audit-depth-item{padding:5px;border-radius:5px;background:#fff;color:#475569;font-size:8px;line-height:1.35}.audit-depth-item[data-result=fail],.audit-depth-item[data-status=finding]{border-left:3px solid #dc2626}.audit-depth-item[data-result=pass],.audit-depth-item[data-status=pass]{border-left:3px solid #16a34a}.audit-depth-item[data-result=not-run],.audit-depth-item[data-status=gap]{border-left:3px solid #f59e0b}
1057
+ .audit-depth-title{display:flex;align-items:flex-start;justify-content:space-between;gap:5px}.audit-depth-links{display:flex;gap:3px;flex-wrap:wrap;margin-top:5px}.audit-depth-action{border:1px solid #cbd5e1;border-radius:4px;background:#fff;padding:2px 5px;color:#334155;font-size:8px;cursor:pointer}.audit-depth-action.primary{border-color:#2563eb;background:#eaf1ff;color:#174ea6}.audit-depth-unlinked{display:inline-block;margin-top:4px;color:#98a2b3;font-size:8px}.finding-context{display:flex;align-items:center;justify-content:space-between;gap:6px;margin:5px 0;padding:5px 6px;border-radius:5px;background:#fff7ed;color:#9a3412;font-size:8px}.finding-context button{border:0;background:transparent;color:#9a3412;font-size:8px;font-weight:700;cursor:pointer}.finding-decision-label{display:inline-flex;align-items:center;gap:3px;margin-top:5px;padding:2px 5px;border-radius:999px;background:#f1f5f9;color:#64748b;font-size:8px;font-weight:700}.finding-card[data-decision=accepted] .finding-decision-label{background:#dcfce7;color:#166534}.finding-card[data-decision=rejected] .finding-decision-label{background:#fee2e2;color:#991b1b}.finding-proposal-note{margin-top:5px;padding:4px 5px;border-radius:5px;background:#f8fafc;color:#64748b;font-size:8px}.fix-queue{margin:9px 0;border:1px solid #bfdbfe;border-radius:8px;background:#eff6ff}.fix-queue>summary{padding:7px 8px;cursor:pointer;color:#1e3a8a;font-size:10px;font-weight:800}.fix-queue-body{padding:0 8px 8px}.fix-queue-copy{margin:0 0 7px;color:#475569;font-size:9px;line-height:1.4}.fix-queue-stats{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:7px}.fix-queue-stat{padding:2px 5px;border-radius:999px;background:#fff;color:#475569;font-size:8px}.fix-queue-actions{display:grid;grid-template-columns:1fr 1fr;gap:5px}.fix-queue-action{min-height:29px;border:1px solid #93c5fd;border-radius:6px;background:#fff;padding:4px 6px;color:#1d4ed8;font-size:9px;cursor:pointer}.fix-queue-action.primary{border-color:#1d4ed8;background:#1d4ed8;color:#fff}.fix-queue-action:disabled{cursor:not-allowed;opacity:.5}.review-workflow-guide{margin:6px 0 8px;padding:6px 7px;border-radius:6px;background:#f8fafc;color:#64748b;font-size:8px;line-height:1.4}.review-workflow-guide b{color:#334155}
1058
+ .revision-badge{padding:1px 4px;border-radius:4px;background:#e2e8f0;color:#64748b;font-size:8px;font-weight:600}.revision-notice{margin:7px 0;padding:7px;border:1px solid #fdba74;border-radius:7px;background:#fff7ed;color:#9a3412;font-size:9px;line-height:1.4}.revision-notice-actions{display:flex;gap:4px;margin-top:6px}.revision-notice button{border:1px solid #fdba74;border-radius:5px;background:#fff;padding:3px 6px;color:#9a3412;font-size:8px;cursor:pointer}.review-phases{display:grid;grid-template-columns:repeat(5,1fr);gap:2px;margin:0 0 7px}.review-phase{min-width:0;padding:4px 2px;border-radius:4px;background:#dbe3ee;color:#64748b;text-align:center;font-size:7px;line-height:1.2}.review-phase.active{background:#2563eb;color:#fff;font-weight:700}.review-phase.complete{background:#dcfce7;color:#166534}.source-handoff{margin-top:7px;padding-top:7px;border-top:1px solid #bfdbfe}.source-handoff-copy{margin:0 0 5px;color:#64748b;font-size:8px;line-height:1.35}
1059
+ .runtime-diagnostics>.diagnostics-body{padding:0 8px 8px}.diagnostics-copy{margin:0 0 7px;color:#64748b;font-size:9px;line-height:1.4}.diagnostics-actions{display:flex;gap:5px;flex-wrap:wrap}.diagnostics-action{min-height:27px;border:1px solid #cbd5e1;border-radius:6px;background:#fff;padding:4px 7px;color:#334155;font-size:9px;cursor:pointer}.diagnostics-action.primary{border-color:#2563eb;background:#2563eb;color:#fff}.diagnostics-action:disabled{cursor:wait;opacity:.55}.diagnostics-status{float:right;padding:1px 5px;border-radius:999px;background:#f1f5f9;color:#64748b;font-size:8px}.diagnostics-status.running{background:#dbeafe;color:#1d4ed8}.diagnostics-status.fail{background:#fee2e2;color:#b91c1c}.diagnostics-status.pass{background:#dcfce7;color:#166534}.diagnostics-progress{margin-top:6px;color:#1d4ed8;font-size:9px}.diagnostics-summary{display:flex;gap:4px;flex-wrap:wrap;margin-top:7px}.diagnostics-count{padding:2px 5px;border-radius:999px;background:#f1f5f9;color:#475569;font-size:8px}.diagnostics-results{display:grid;gap:4px;margin-top:7px}.diagnostic-card{border:1px solid #e2e8f0;border-left-width:3px;border-radius:6px;background:#fff;padding:6px;color:#475569;font-size:8px;line-height:1.35}.diagnostic-card[data-result=pass]{border-left-color:#16a34a}.diagnostic-card[data-result=fail]{border-left-color:#dc2626}.diagnostic-card[data-result=warning]{border-left-color:#f59e0b}.diagnostic-card b{color:#1e293b}.diagnostic-meta{margin-top:2px;color:#94a3b8}.diagnostic-card-actions{display:flex;gap:3px;flex-wrap:wrap;margin-top:5px}.diagnostic-card-action{border:1px solid #cbd5e1;border-radius:4px;background:#fff;padding:2px 5px;color:#334155;font-size:8px;cursor:pointer}.diagnostic-card-action.primary{border-color:#2563eb;background:#eaf1ff;color:#174ea6}.diagnostics-empty{margin:7px 0 0;color:#64748b;font-size:9px}.ui-node[data-diagnostic-target=true]{outline:3px solid #7c3aed!important;outline-offset:2px}
1060
+ @container(max-width:900px){.mode-status{display:none}.screen-context{max-width:130px}.mode-button,.interaction-button{padding-inline:6px}.mode-button .button-icon,.interaction-button .button-icon{margin-right:2px}}@container(max-width:690px){.screen-context{display:none}.mode-button .button-label,.interaction-button .button-label{display:none}.mode-button,.interaction-button{width:30px;padding:0}}@media(max-width:820px){.review-app{--right-panel:0px;--right-grip:0px}.inspector,.right-resizer{display:none}.compare-grid{grid-template-columns:1fr}}@media(max-width:620px){.review-app{--left-panel:0px;--left-grip:0px;display:grid}.sidebar,.left-resizer{display:none}.menu-strip{display:none}.action-bar{overflow-x:auto}.stage{padding:16px}}
1061
+
1062
+ /* Production workbench shell */
1063
+ .icon-sprite{position:absolute;width:0;height:0;overflow:hidden}.icon,.app-logo,.empty-icon{display:block;width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}.icon use,.app-logo use,.empty-icon use{pointer-events:none}
1064
+ .review-app{--rail:44px;--left-panel:244px;--right-panel:324px;--left-grip:4px;--right-grip:4px;grid-template-columns:var(--rail) var(--left-panel) var(--left-grip) minmax(360px,1fr) var(--right-grip) var(--right-panel);background:#f4f5f7;color:#18202b}
1065
+ .workbench-rail{position:relative;z-index:25;height:100vh;display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 6px;background:#171b24;color:#a9b1bf;border-right:1px solid #0e1118}.rail-button{position:relative;width:32px;height:32px;display:grid;place-items:center;border:0;border-radius:7px;background:transparent;color:inherit;cursor:pointer}.rail-button:hover{background:#272d39;color:#fff}.rail-button[aria-pressed=true]{background:#30394a;color:#fff}.rail-button[aria-pressed=true]:before{content:'';position:absolute;left:-6px;top:8px;bottom:8px;width:2px;border-radius:2px;background:#7aa2ff}.rail-divider{width:22px;height:1px;margin:3px 0;background:#313744}.rail-spacer{flex:1}.rail-badge{position:absolute!important;right:-2px;top:-2px;min-width:14px;height:14px;display:grid;place-items:center;padding:0 3px!important;border:2px solid #171b24!important;border-radius:999px!important;background:#3b82f6!important;color:#fff!important;font-size:7px!important;line-height:1!important}.annotation-total:empty,.rail-badge:empty{display:none}
1066
+ .sidebar,.inspector{height:100vh;padding:0;background:#fff;border-color:#dfe3e8;overflow:hidden}.sidebar{display:flex;flex-direction:column}.side-panel-header,.inspector-header{height:44px;flex:0 0 44px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 10px;border-bottom:1px solid #e7e9ed;background:#fff}.panel-kicker{display:block;color:#8a93a2;font-size:8px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.side-panel-title,.pane-heading h2{margin:1px 0 0;color:#1d2430;font-size:13px;line-height:1.1}.panel-close,.header-panel-button{width:28px;height:28px;display:grid;place-items:center;border:0;border-radius:6px;background:transparent;color:#707a89;cursor:pointer}.panel-close:hover,.header-panel-button:hover{background:#f0f2f5;color:#1f2937}.sidebar-project{margin:10px 12px 7px;overflow:hidden;color:#4b5563;font-size:10px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.tree-search{height:30px;display:flex;align-items:center;gap:7px;margin:0 10px 8px;padding:0 8px;border:1px solid #dfe3e8;border-radius:7px;background:#f8f9fb;color:#8a93a2}.tree-search .icon{width:14px;height:14px}.tree-search input{min-width:0;width:100%;border:0;outline:0;background:transparent;color:#202733;font-size:10px}.tree-search:focus-within{border-color:#8fb3ff;background:#fff;box-shadow:0 0 0 2px rgba(59,130,246,.12)}.screen-list{flex:1;overflow:auto;padding:0 8px 12px}.sidebar-meta{flex:0 0 auto;max-height:30%;overflow:auto;padding:8px;border-top:1px solid #eceef1;background:#fafbfc}.audit-panel{margin:0;border:0;background:transparent}.audit-panel>summary{padding:5px;color:#687384}.audit-panel .audit{padding:0 5px 6px}.warning-panel{margin:6px 4px 0;padding-top:6px}
1067
+ .screen-tree-group>summary{min-height:26px;padding:4px 6px;border-radius:6px;color:#596474;font-size:10px}.screen-tree-children{margin-left:8px;padding-left:7px;border-left-color:#e5e8ec}.screen-link{min-height:27px;padding:5px 22px 5px 8px!important;border-radius:6px;color:#394454;font-size:10px;line-height:1.25}.screen-link:hover{background:#f2f4f7}.screen-link[aria-current=true]{background:#e9f0ff;color:#2457b8}.screen-link[aria-current=true]:before{left:2px;top:6px;bottom:6px}.tree-count{background:transparent;color:#9aa2af}
1068
+ .panel-resizer{width:auto;background:#eef0f3}.panel-resizer:after{inset:0 -4px}.panel-resizer:hover,.panel-resizer:focus-visible,.review-app.resizing .panel-resizer{background:#7aa2ff}
1069
+ .workspace{background:#f4f5f7}.workbench-header{height:44px;box-shadow:none;border-bottom:1px solid #dfe3e8;background:rgba(255,255,255,.96)}.menu-bar{height:44px;gap:6px;padding:0 8px 0 10px;border:0;background:transparent}.app-mark{height:30px;gap:7px;padding-right:9px;color:#222a36;font-size:11px}.app-logo{width:18px;height:18px;padding:3px;border-radius:5px;background:#202735;color:#fff;stroke-width:1.35}.revision-badge{padding:1px 4px;background:#eef0f3;color:#8992a0;font-size:7px}.menu-strip{height:30px;align-self:center}.menu>summary{height:30px;padding:0 7px;border-radius:5px;color:#657080;font-size:9px}.menu>summary:hover,.menu[open]>summary{background:#eff1f4;color:#242c38}.menu-popover{top:33px;min-width:190px;padding:5px;border-color:#d9dde3;border-radius:9px;box-shadow:0 12px 34px rgba(25,31,42,.18)}.menu-popover button{min-height:30px;padding:6px 9px;border-radius:6px;font-size:10px}.screen-context{min-width:0;max-width:min(360px,32vw);display:block;margin-left:4px;padding-left:10px;border-left:1px solid #e3e6ea}.screen-title{overflow:hidden;color:#303947;font-size:10px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.navigation-preview-label{font-size:8px;color:#b56a13}.version-select{height:28px;max-width:190px;padding:0 26px 0 8px;border-color:#d9dde3;border-radius:7px;background:#f8f9fb;color:#465160;font-size:9px}.header-panel-button{flex:0 0 auto}.mode-status{height:24px;padding:0 8px;border:0;background:#f0f2f5;color:#657080;font-size:8px}.mode-status:before{width:5px;height:5px}.mode-status.prototype{background:#e9f7ef;color:#25613d}.mode-status.prototype:before{box-shadow:none}
1070
+ .language-menu>summary{min-width:34px;justify-content:center;font-weight:750;letter-spacing:.04em}.language-menu .menu-popover{left:auto;right:0;min-width:150px}.locale-option{position:relative;padding-right:28px!important}.locale-option[aria-checked=true]{background:#eef3fb!important;color:#2f5fb7!important;font-weight:700}.locale-option[aria-checked=true]:after{content:'✓';position:absolute;right:10px;top:50%;transform:translateY(-50%)}
1071
+ .stage{padding:46px 40px 72px;background-color:#f1f2f4;background-size:20px 20px;background-position:0 0,0 10px,10px -10px,-10px 0}.canvas-tools{position:absolute;left:50%;bottom:14px;z-index:18;display:flex;align-items:center;gap:5px;transform:translateX(-50%);padding:4px;border:1px solid #d8dce2;border-radius:10px;background:rgba(255,255,255,.96);box-shadow:0 8px 28px rgba(26,32,44,.15);backdrop-filter:blur(10px)}.mode-switch,.interaction-switch{gap:2px;padding:0;background:transparent}.mode-button,.interaction-button,.compare-button{height:30px;display:flex;align-items:center;justify-content:center;gap:5px;padding:0 8px;border-radius:7px;color:#657080;font-size:9px}.mode-button .icon,.interaction-button .icon{width:15px;height:15px}.mode-button:hover,.interaction-button:hover{background:#f0f2f5;color:#26303d}.mode-button[aria-pressed=true],.interaction-button[aria-pressed=true]{background:#273142;color:#fff;font-weight:650;box-shadow:none}.tool-divider{width:1px;height:20px;background:#e1e4e8}.compare-controls{position:absolute;left:50%;top:54px;z-index:17;transform:translateX(-50%);gap:2px;padding:3px;border:1px solid #d8dce2;border-radius:8px;background:#fff;box-shadow:0 5px 16px rgba(26,32,44,.1)}.compare-button{height:27px}.compare-button[aria-pressed=true]{background:#273142;color:#fff}.review-app[data-view=compare] .screen-context{display:block}.review-app[data-view=compare] .interaction-button{width:auto;padding:0 8px}.review-app[data-view=compare] .interaction-button .button-label{display:inline}.floating-zoom{right:14px;bottom:14px;gap:1px;padding:3px;border-color:#d8dce2;border-radius:9px;background:rgba(255,255,255,.96);box-shadow:0 6px 20px rgba(26,32,44,.12)}.zoom-button{height:28px;min-width:28px;border-radius:6px;color:#596474}.zoom-button .icon{width:14px;height:14px;margin:auto}.zoom-label{min-width:42px;font-size:9px;font-variant-numeric:tabular-nums}.workbench-toast{bottom:58px;border-radius:8px;background:#202735;font-size:10px}
1072
+ .inspector{display:flex;flex-direction:column}.inspector-header{padding-left:7px}.inspector-tabs{min-width:0;display:flex;align-self:stretch;gap:2px}.inspector-tab{position:relative;min-width:0;height:43px;display:flex;align-items:center;gap:5px;border:0;border-bottom:2px solid transparent;background:transparent;padding:0 7px;color:#707a89;font-size:9px;cursor:pointer}.inspector-tab:hover{color:#28313e}.inspector-tab[aria-pressed=true]{border-bottom-color:#3b6fd8;color:#244f9f;font-weight:700}.inspector-tab .finding-total,.inspector-tab .annotation-total{min-width:15px;padding:1px 4px;border-radius:999px;background:#eef0f3;color:#687384;font-size:7px;text-align:center}.inspector-pane{flex:1;min-height:0;overflow:auto;padding:10px}.inspector-pane[hidden]{display:none}.pane-heading{display:flex;align-items:center;justify-content:space-between;margin:2px 2px 10px}.pane-heading>.annotation-total{padding:2px 6px;border-radius:999px;background:#eef0f3;color:#687384;font-size:8px}.panel-empty{min-height:220px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:24px;color:#8a93a2;text-align:center;font-size:10px;line-height:1.4}.panel-empty strong{color:#465160;font-size:11px}.empty-icon{width:24px;height:24px;color:#a8b0bc}.inspect-table{grid-template-columns:76px 1fr;gap:8px 9px;padding:4px;font-size:9px}.inspect-table dt{color:#8a93a2}.inspect-table dd{overflow-wrap:anywhere;color:#35404f}.comment-form{gap:8px;margin-top:14px;padding-top:12px}.comment-form textarea{min-height:92px;border-color:#d9dde3;border-radius:8px;padding:9px;font-size:10px}.comment-form select{height:30px;border-color:#d9dde3;border-radius:7px;font-size:9px}.comment-form button{min-height:32px;border-radius:7px;background:#315fbd;font-size:9px}
1073
+ .revision-notice{margin:0 0 9px;padding:9px;border-color:#f2c879;border-radius:8px;background:#fff8e9;color:#825418;font-size:9px}.revision-notice-actions button{min-height:26px;padding:3px 7px;border-radius:6px}.fix-queue{margin:0 0 10px;border-color:#dce3ef;border-radius:9px;background:#f7f9fc}.fix-queue>summary,.panel-section>summary{min-height:34px;display:flex;align-items:center;justify-content:space-between;padding:7px 9px;list-style:none;color:#344054;font-size:10px;font-weight:700}.fix-queue>summary::-webkit-details-marker,.panel-section>summary::-webkit-details-marker{display:none}.fix-queue>summary:after,.panel-section>summary:after{content:'›';margin-left:auto;color:#9aa2af;transform:rotate(90deg)}.fix-queue:not([open])>summary:after,.panel-section:not([open])>summary:after{transform:none}.fix-queue-count{min-width:18px;margin-left:6px;padding:2px 5px;border-radius:999px;background:#e5eaf2;color:#526071;font-size:8px;text-align:center}.fix-queue-body{padding:0 9px 9px}.review-phases{display:grid;grid-template-columns:1fr;gap:5px;margin:4px 0 9px}.review-phase{position:relative;min-height:40px;display:grid;grid-template-columns:24px 1fr;grid-template-rows:auto auto;align-items:center;column-gap:8px;padding:6px 8px;border:1px solid #e1e5eb;border-radius:8px;background:#fff;color:#687586;text-align:left;font-size:8px;line-height:1.25}.review-phase:before{content:attr(data-step);grid-row:1/3;width:22px;height:22px;display:grid;place-items:center;border-radius:50%;background:#eef1f5;color:#697586;font-size:8px;font-weight:800}.review-phase:after{display:none}.review-phase b{color:#344054;font-size:9px}.review-phase small{color:#8993a1;font-size:7px}.review-phase.active{border-color:#9db8eb;background:#f2f6ff;color:#2f5fb7}.review-phase.active:before{background:#315fbd;color:#fff}.review-phase.complete{border-color:#b8d8c4;background:#f1faf4;color:#3a7654}.review-phase.complete:before{background:#4f9a70;color:#fff}.review-phase.complete b{color:#326a49}.fix-queue-stats{margin-bottom:8px}.fix-queue-stat{background:#fff;border:1px solid #e1e5ea;color:#697586;font-size:7px}.fix-queue-actions{gap:6px}.fix-queue-action{min-height:31px;display:flex;align-items:center;justify-content:center;gap:5px;border-color:#d5dbe4;border-radius:7px;color:#45566f;font-size:8px}.fix-queue-action .icon{width:13px;height:13px}.fix-queue-action.primary{border-color:#315fbd;background:#315fbd}.source-handoff{margin-top:7px;padding-top:7px;border-top-color:#e1e6ed}.source-handoff .fix-queue-action{width:100%}
1074
+ .automated-review{margin:0;padding:10px 0 0;border-top:1px solid #e7e9ed}.review-section-heading{margin:0 2px 8px}.review-section-heading h2{font-size:11px}.review-section-actions{display:flex;align-items:center;gap:5px;margin-left:auto}.review-section-actions .canvas-layer-toggle{width:28px;height:26px;padding:0}.review-section-actions .canvas-layer-count{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.finding-total{padding:2px 6px;background:#eef0f3;color:#687384}.audit-summary{font-size:9px}.audit-count{background:#f2f4f7;font-size:7px}.audit-depth{border-color:#e4e7eb;background:#fafbfc}.finding-filters{gap:4px;margin:8px 0;padding-bottom:2px}.finding-filter{min-height:25px;padding:3px 7px;border-color:#dfe3e8;border-radius:999px;font-size:7px}.finding-filter[aria-pressed=true]{border-color:#9eb9ee;background:#edf3ff;color:#2857ac}.finding-list{gap:8px}.finding-card{padding:9px;border-color:#e0e4e9;border-left-width:3px;border-radius:8px;box-shadow:0 1px 1px rgba(22,29,40,.02)}.finding-title{font-size:10px}.finding-severity{border-radius:999px;font-size:6px}.finding-meta{font-size:7px}.finding-observation,.finding-impact,.finding-recommendation{font-size:8px;line-height:1.45}.finding-proposal-note{padding:5px 6px;border-radius:6px;font-size:7px}.finding-action{min-height:25px;display:inline-flex;align-items:center;gap:4px;padding:3px 6px;border-color:#d8dde4;border-radius:6px;font-size:7px}.finding-action.primary{border-color:#315fbd;background:#315fbd}.panel-section{margin:10px 0;border-color:#e1e5ea;border-radius:8px}.runtime-diagnostics>.diagnostics-body{padding:0 9px 9px}.diagnostics-action{min-height:29px;display:flex;align-items:center;gap:5px;border-radius:7px}.diagnostics-action .icon{width:13px;height:13px}.diagnostics-action.primary{border-color:#315fbd;background:#315fbd}.review-footer{position:sticky;bottom:-10px;margin:12px -10px -10px;padding:8px 10px 10px;border-top:1px solid #e1e5ea;background:rgba(255,255,255,.96);backdrop-filter:blur(8px)}.review-decision{margin:0 0 7px;padding:0;background:transparent;color:#7c8796;font-size:8px}.review-actions{display:grid;grid-template-columns:1fr 1fr 32px;gap:6px;margin:0}.review-action{min-height:32px;display:flex;align-items:center;justify-content:center;gap:5px;border-color:#d6dce4;border-radius:7px;font-size:8px}.review-action .icon{width:13px;height:13px}.review-action.primary{border-color:#315fbd;background:#315fbd}.review-action.icon-only{padding:0}.annotation-list{gap:8px}.annotation-card{padding:9px;border-color:#e0e4e9;border-radius:8px}.annotation-text{font-size:9px}.annotation-total{font-variant-numeric:tabular-nums}
1075
+ .panel-empty.compact{min-height:132px;margin-bottom:10px;border:1px dashed #dce1e7;border-radius:8px}.comment-form{gap:8px;margin:0 0 12px;padding:10px;border:1px solid #dfe4ea;border-radius:9px;background:#fafbfc}.comment-target{overflow:hidden;color:#6f7a89;font-size:8px;text-overflow:ellipsis;white-space:nowrap}.comment-form-row{display:grid;grid-template-columns:1fr 76px;gap:6px}.comment-form-row button{margin:0}.finding-details{margin-top:6px;border-top:1px solid #eceef1;padding-top:5px}.finding-details>summary{cursor:pointer;color:#667386;font-size:8px;font-weight:650}.finding-details[open]>summary{margin-bottom:4px}.finding-action .icon{width:12px;height:12px}.rail-badge[hidden]{display:none!important}
1076
+ .review-launcher{margin:0 0 10px;padding:11px;border:1px solid #d8e1f0;border-radius:10px;background:linear-gradient(145deg,#f8fbff,#f3f6fb)}.review-launcher-head{display:flex;align-items:flex-start;gap:8px}.review-launcher-icon{width:28px;height:28px;flex:0 0 28px;display:grid;place-items:center;border-radius:8px;background:#e6efff;color:#2f5fb7}.review-launcher-icon .icon{width:15px;height:15px}.review-launcher-title{margin:0;color:#202936;font-size:11px;line-height:1.25}.review-launcher-meta{margin:2px 0 0;color:#788494;font-size:8px}.review-launcher-status{min-height:16px;margin:8px 0;color:#536174;font-size:9px;line-height:1.4}.review-launcher[data-state=complete] .review-launcher-status{color:#315f55}.review-launcher-actions{display:grid;gap:6px}.review-launcher-action{min-height:33px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid #d4dae3;border-radius:7px;background:#fff;color:#455366;font-size:9px;font-weight:650;cursor:pointer}.review-launcher-action.primary{border-color:#315fbd;background:#315fbd;color:#fff}.review-launcher-action:hover:not(:disabled){border-color:#9eb6df;background:#f7faff}.review-launcher-action.primary:hover:not(:disabled){border-color:#284f9e;background:#284f9e}.review-launcher-action:disabled{cursor:not-allowed;opacity:.52}.review-launcher-action .icon{width:14px;height:14px}.review-launcher-progress{height:3px;overflow:hidden;margin:-2px 0 8px;border-radius:999px;background:#dfe7f3}.review-launcher-progress:after{content:'';display:block;width:38%;height:100%;border-radius:inherit;background:#3b6fd8;animation:review-progress 1s ease-in-out infinite alternate}.review-launcher:not([data-state=running]) .review-launcher-progress{display:none}@keyframes review-progress{from{transform:translateX(-20%)}to{transform:translateX(210%)}}
1077
+ .review-subtabs{position:sticky;top:-10px;z-index:8;display:grid;grid-template-columns:repeat(3,1fr);gap:3px;margin:-10px -10px 10px;padding:8px 10px;border-bottom:1px solid #e4e7ec;background:rgba(255,255,255,.96);backdrop-filter:blur(8px)}.review-subtab{min-width:0;height:29px;border:0;border-radius:7px;background:transparent;color:#667386;font-size:8px;font-weight:650;cursor:pointer}.review-subtab:hover{background:#f1f3f6;color:#2c3440}.review-subtab[aria-pressed=true]{background:#e9f0ff;color:#2857ac}.review-subtab-count{margin-left:3px;color:#8d97a5;font-size:7px;font-variant-numeric:tabular-nums}.review-workspace[hidden]{display:none}.review-scope-row{display:grid;grid-template-columns:1fr 104px;align-items:center;gap:8px;margin:8px 0 0}.review-scope-label{color:#6c7888;font-size:8px}.review-scope-select,.finding-select{height:28px;min-width:0;border:1px solid #d8dee7;border-radius:7px;background:#fff;color:#465264;padding:0 7px;font-size:8px}.coverage-panel,.review-history,.import-review-panel{margin:0 0 10px;padding:10px;border:1px solid #e0e4e9;border-radius:9px;background:#fff}.coverage-head,.history-head,.import-review-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.coverage-head h3,.history-head h3,.import-review-head h3{margin:0;color:#293443;font-size:10px}.coverage-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.coverage-metric{padding:7px;border-radius:7px;background:#f5f7fa}.coverage-metric b{display:block;color:#263243;font-size:13px;font-variant-numeric:tabular-nums}.coverage-metric span{color:#7b8796;font-size:7px}.coverage-gaps{margin:8px 0 0;padding:7px;border-radius:7px;background:#fff7e8;color:#8b5a13;font-size:8px;line-height:1.4}.coverage-gaps.pass{background:#edf8f1;color:#326648}.coverage-matrix{margin-top:8px;display:grid;gap:4px}.coverage-row{display:grid;grid-template-columns:minmax(70px,1fr) 42px 42px 42px;align-items:center;gap:3px;color:#657181;font-size:7px}.coverage-row.header{color:#929aa6}.coverage-cell{padding:3px;border-radius:4px;background:#eef1f5;text-align:center;font-variant-numeric:tabular-nums}.coverage-cell.pass{background:#e6f5eb;color:#2f6c48}.coverage-cell.gap{background:#fff0d6;color:#8b5a13}.review-history-list{display:grid;gap:5px}.review-history-item{display:grid;grid-template-columns:1fr auto;gap:5px;padding:6px;border-radius:7px;background:#f6f7f9;color:#4f5c6c;font-size:8px}.review-history-item small{grid-column:1/-1;color:#8a94a2}.history-clear{border:0;background:transparent;color:#7b8796;font-size:7px;cursor:pointer}.finding-toolbar{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin:7px 0}.finding-bulk{display:grid;grid-template-columns:1fr 1.35fr auto;gap:5px;margin:6px 0}.finding-bulk button,.import-review-action{min-height:29px;border:1px solid #d8dee7;border-radius:6px;background:#fff;color:#4b596b;padding:4px 7px;font-size:7px;cursor:pointer}.finding-bulk .fix-all-source{border-color:#315fbd;background:#315fbd;color:#fff;font-weight:700}.finding-group-label{margin:7px 0 3px;color:#7c8796;font-size:7px;font-weight:700;text-transform:uppercase}.import-review-copy{margin:0 0 8px;color:#6d7989;font-size:8px;line-height:1.4}.import-review-actions{display:grid;grid-template-columns:1fr 1fr;gap:5px}.import-review-status{margin-top:7px;color:#607083;font-size:8px}.review-context-footer{position:sticky;bottom:-10px;z-index:9;margin:12px -10px -10px;padding:8px 10px 10px;border-top:1px solid #dfe3e8;background:rgba(255,255,255,.96);backdrop-filter:blur(8px)}.review-context-status{margin:0 0 6px;color:#7a8594;font-size:8px}.review-context-actions{display:grid;grid-template-columns:1fr auto auto;gap:5px}.review-next-action,.review-secondary-action{min-height:33px;display:flex;align-items:center;justify-content:center;gap:5px;border:1px solid #d4dae3;border-radius:7px;background:#fff;color:#526073;padding:0 9px;font-size:8px;cursor:pointer}.review-next-action{border-color:#315fbd;background:#315fbd;color:#fff;font-weight:700}.review-next-action:disabled{opacity:.5;cursor:not-allowed}.review-secondary-action.icon-only{width:33px;padding:0}.screen-card{content-visibility:auto;contain-intrinsic-size:320px 620px}
1078
+ .agent-handoff-panel{margin:0 0 10px;padding:9px;border:1px solid #cfdcf3;border-radius:9px;background:#f5f8ff}.agent-handoff-panel[hidden]{display:none}.agent-handoff-head{display:flex;align-items:center;gap:7px;color:#2857ac}.agent-handoff-head .icon{width:14px;height:14px}.agent-handoff-status{font-size:9px;font-weight:700}.agent-handoff-meta{margin:5px 0 8px;color:#657386;font-size:8px;line-height:1.4}.agent-handoff-actions{display:grid;grid-template-columns:1fr 1fr;gap:5px}.agent-handoff-action{min-height:28px;border:1px solid #ccd7e7;border-radius:6px;background:#fff;color:#46566c;font-size:7px;cursor:pointer}.review-launcher-action.icon-only{min-height:29px}.review-launcher-actions.with-handoff{grid-template-columns:1fr 34px}.review-launcher-actions.with-handoff .run-review{grid-column:1/-1}
1079
+ .review-subtabs{gap:5px;padding-top:7px;padding-bottom:7px}.review-subtab{height:46px;display:grid;grid-template-columns:22px minmax(0,1fr);align-items:center;gap:5px;padding:4px 5px;text-align:left}.review-subtab-step{width:20px;height:20px;display:grid;place-items:center;border-radius:999px;background:#eef1f5;color:#748091;font-size:8px;font-weight:800}.review-subtab>span:last-child{min-width:0;display:grid;gap:1px}.review-subtab b{overflow:hidden;color:inherit;font-size:8px;text-overflow:ellipsis;white-space:nowrap}.review-subtab small{overflow:hidden;color:#929baa;font-size:6px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}.review-subtab[aria-pressed=true] .review-subtab-step{background:#315fbd;color:#fff}.review-subtab.complete .review-subtab-step{background:#4f9a70;color:#fff}.review-subtab.complete:not([aria-pressed=true]){background:#f1faf4;color:#326a49}.review-subtab.complete small{color:#568069}
1080
+ .review-launcher{padding:12px}.review-launcher-title{font-size:12px}.review-launcher-meta{font-size:8px}.review-launcher-status{margin-bottom:2px;font-size:10px}.review-scope-row{margin-top:10px}.review-advanced-tools,.finding-filter-panel,.review-danger-zone{margin-top:9px;border-top:1px solid #e1e5eb;padding-top:8px}.review-advanced-tools>summary,.finding-filter-panel>summary,.review-danger-zone>summary{cursor:pointer;color:#667386;font-size:8px;font-weight:700}.review-advanced-tools p,.review-danger-zone p{margin:6px 0;color:#7a8594;font-size:8px;line-height:1.4}.review-advanced-tools .review-launcher-actions{grid-template-columns:1fr 34px;margin-top:7px}
1081
+ .coverage-panel,.review-history,.import-review-panel{padding:0;overflow:hidden}.coverage-panel>summary,.review-history>summary,.import-review-panel>summary{min-height:36px;display:flex;align-items:center;justify-content:space-between;padding:8px 10px;cursor:pointer;list-style:none;color:#344054;font-size:9px;font-weight:700}.coverage-panel>summary::-webkit-details-marker,.review-history>summary::-webkit-details-marker,.import-review-panel>summary::-webkit-details-marker{display:none}.coverage-panel>summary:after,.review-history>summary:after,.import-review-panel>summary:after{content:'›';color:#9aa2af}.coverage-panel[open]>summary:after,.review-history[open]>summary:after,.import-review-panel[open]>summary:after{transform:rotate(90deg)}.coverage-body,.review-history>.history-head,.review-history-list,.import-review-panel>.import-review-head,.import-review-panel>.import-review-copy,.import-review-panel>.import-review-actions,.import-review-panel>.import-review-status{margin-left:10px;margin-right:10px}.coverage-body,.review-history-list{margin-bottom:10px}.review-history>.history-head{margin-bottom:8px}.review-session-name{min-width:0;width:100%;height:28px;border:1px solid #d8dee7;border-radius:7px;padding:0 7px;font-size:8px}.import-review-panel>.import-review-status{margin-bottom:9px}
1082
+ .problem-overview{display:grid;gap:3px;margin-bottom:8px;padding:9px;border-radius:8px;background:#f4f7fb;color:#566476}.problem-overview b{color:#263243;font-size:11px}.problem-overview span{font-size:8px;line-height:1.4}.problem-overview.complete{background:#edf8f1;color:#326648}.problem-overview.complete b{color:#285b3f}.audit-details{margin:0 0 8px}.audit-details>summary{cursor:pointer;color:#6e7988;font-size:8px;font-weight:700}.finding-filter-panel{margin:0 0 8px;padding:7px 8px;border:1px solid #e1e5ea;border-radius:8px}.finding-filter-panel .finding-toolbar{margin-top:7px}.finding-filter-panel .finding-filters{margin-bottom:0}.finding-bulk{grid-template-columns:1fr 1fr}.finding-card-status{display:inline-flex;align-items:center;margin-top:5px;padding:2px 6px;border-radius:999px;background:#f1f5f9;color:#64748b;font-size:7px;font-weight:700}.finding-card-status.resolved{background:#dcfce7;color:#166534}.finding-card-status.selected{background:#eaf1ff;color:#174ea6}.finding-more{display:inline-block;position:relative}.finding-more>summary{min-height:25px;display:flex;align-items:center;padding:3px 6px;border:1px solid #d8dde4;border-radius:6px;cursor:pointer;list-style:none;color:#526073;font-size:7px}.finding-more>summary::-webkit-details-marker{display:none}.finding-more-actions{display:flex;gap:4px;margin-top:4px}.review-danger-zone{margin:10px 0;padding:8px;border:1px solid #f0d5d5;border-radius:8px;background:#fffafa}.review-danger-zone>summary{color:#8f4a4a}.review-danger-zone .fix-all-source{width:100%;min-height:30px;border:1px solid #d9a6a6;border-radius:7px;background:#fff;color:#a13d3d;font-size:8px;cursor:pointer}
1083
+ .review-context-status{font-size:9px;line-height:1.35}.review-next-action{min-height:36px;font-size:9px}.review-context-actions{grid-template-columns:1fr auto}.review-secondary-action{min-height:36px}.fix-queue-copy{font-size:8px}.fix-queue-actions{grid-template-columns:1fr}.import-review-panel{background:#fafbfc}
1084
+ .screen-card{content-visibility:visible;contain-intrinsic-size:auto}
1085
+ .canvas-layer-controls{position:absolute;left:50%;top:54px;z-index:19;display:flex;align-items:center;gap:3px;transform:translateX(-50%);min-height:36px;padding:3px;border:1px solid #d8dce2;border-radius:9px;background:rgba(255,255,255,.97);box-shadow:0 5px 18px rgba(26,32,44,.12);backdrop-filter:blur(10px)}.canvas-layer-toggle,.version-visibility-button,.compare-button{height:28px;display:inline-flex;align-items:center;justify-content:center;gap:5px;border:0;border-radius:6px;background:transparent;padding:0 8px;color:#657080;font-size:8px;font-weight:650;cursor:pointer;white-space:nowrap}.canvas-layer-toggle:hover:not(:disabled),.version-visibility-button:hover:not(:disabled),.compare-button:hover:not(:disabled){background:#f0f2f5;color:#26303d}.canvas-layer-toggle[aria-pressed=true]{background:#e9f0ff;color:#2857ac;box-shadow:inset 0 0 0 1px #bfd0f2}.version-visibility-button[aria-pressed=true],.compare-button[aria-pressed=true]{background:#273142;color:#fff}.canvas-layer-toggle:disabled,.version-visibility-button:disabled,.compare-button:disabled{cursor:not-allowed;opacity:.38}.canvas-layer-toggle .icon{width:13px;height:13px}.canvas-layer-count{min-width:14px;padding:1px 4px;border-radius:999px;background:#e6e9ee;color:#596474;font-size:7px;font-variant-numeric:tabular-nums}.canvas-layer-toggle[aria-pressed=true] .canvas-layer-count{background:#d5e3ff;color:#2857ac}.canvas-layer-divider{width:1px;height:18px;margin:0 2px;background:#e1e4e8}.version-visibility-controls,.compare-layout-controls{display:flex;gap:2px;padding:2px;border-radius:7px;background:#f1f3f6}.version-visibility-button{min-width:47px}.compare-layout-controls{min-width:112px}.compare-layout-controls .compare-button{flex:1}.compare-layout-controls[aria-disabled=true]{opacity:.48}.compare-layout-controls[aria-disabled=true] .compare-button{pointer-events:none}
1086
+ .finding-canvas-layer{position:absolute;left:0;top:0;z-index:18;pointer-events:none;contain:layout style}.finding-pin-leader{position:absolute;height:1px;transform-origin:left center;background:rgba(76,91,115,.55);opacity:0;pointer-events:none;transition:opacity .12s}.finding-pin-leader:has(+ .finding-pin:hover),.finding-pin-leader:has(+ .finding-pin:focus-visible),.finding-pin-leader:has(+ .finding-pin-card){opacity:1}.finding-pin,.finding-pin-card{position:absolute;pointer-events:auto;touch-action:manipulation}.finding-pin{width:24px;height:24px;display:grid;place-items:center;transform:translate(-50%,-50%);border:2px solid #fff;border-radius:999px;background:#dc2626;color:#fff;box-shadow:0 2px 8px rgba(15,23,42,.35);font-size:9px;font-weight:800;cursor:pointer;font-variant-numeric:tabular-nums}.finding-pin[data-severity=blocker]{background:#7f1d1d}.finding-pin[data-severity=medium]{background:#d97706}.finding-pin[data-severity=low]{background:#2563eb}.finding-pin:hover,.finding-pin:focus-visible{transform:translate(-50%,-50%) scale(1.12);outline:3px solid rgba(147,197,253,.8);outline-offset:2px}.finding-pin-card{width:min(280px,calc(100vw - 48px));overflow:hidden;border:1px solid #d7dce3;border-radius:10px;background:#fff;box-shadow:0 14px 38px rgba(20,27,38,.24);color:#303947}.finding-pin-card-header{display:grid;grid-template-columns:24px minmax(0,1fr) 26px;align-items:start;gap:7px;padding:9px 8px 7px}.finding-pin-number{width:22px;height:22px;display:grid;place-items:center;border-radius:999px;background:#dc2626;color:#fff;font-size:8px;font-weight:800}.finding-pin-card-title{margin:1px 0 0;font-size:10px;line-height:1.35}.finding-pin-collapse{width:26px;height:26px;display:grid;place-items:center;border:0;border-radius:6px;background:#f0f2f5;color:#5c6776;cursor:pointer}.finding-pin-collapse:hover{background:#e4e7eb;color:#202936}.finding-pin-collapse .icon{width:12px;height:12px}.finding-pin-card-body{padding:0 10px 10px}.finding-pin-card-meta{margin:0 0 6px;color:#788392;font-size:7px;text-transform:uppercase}.finding-pin-card-body p{margin:5px 0;color:#4f5c6c;font-size:9px;line-height:1.45}.finding-pin-card-body b{color:#2e3846}.finding-pin-card-actions{display:flex;gap:5px;margin-top:8px}.finding-pin-card-actions button{min-height:27px;border:1px solid #d5dbe3;border-radius:6px;background:#fff;padding:4px 7px;color:#465466;font-size:8px;cursor:pointer}.finding-pin-card-actions button.primary{border-color:#315fbd;background:#315fbd;color:#fff}.finding-card-header{display:grid;grid-template-columns:22px minmax(0,1fr) auto}.finding-list-index{width:20px;height:20px;display:grid;place-items:center;border-radius:999px;background:#eef0f3;color:#596474;font-size:7px;font-weight:800;font-variant-numeric:tabular-nums}
1087
+ .review-session-name{width:100%;height:28px;margin:0 0 7px;border:1px solid #dde2e8;border-radius:7px;background:#fafbfc;color:#465160;padding:0 7px;font-size:8px}
1088
+ @container(max-width:760px){.screen-context{display:none}.menu-strip{display:none}.mode-status{display:none}.canvas-tools .button-label{display:none}.canvas-tools .mode-button,.canvas-tools .interaction-button{width:30px;padding:0}.canvas-tools{gap:3px}.canvas-layer-toggle{padding:0 7px}.canvas-layer-toggle .layer-label{display:none}}
1089
+ @media(max-width:980px){.review-app{grid-template-columns:var(--rail) var(--left-panel) var(--left-grip) minmax(0,1fr)}.right-resizer{display:none}.inspector{display:flex;position:fixed;right:0;top:0;z-index:60;width:min(360px,calc(100vw - var(--rail)));box-shadow:-12px 0 36px rgba(24,31,43,.18)}.review-app.inspector-hidden>.inspector{display:none}.compare-grid{grid-template-columns:1fr}.header-panel-button{display:grid}}
1090
+ @media(max-width:720px){.review-app{grid-template-columns:var(--rail) minmax(0,1fr)}.sidebar{display:flex;position:fixed;left:var(--rail);top:0;z-index:60;width:min(280px,calc(100vw - var(--rail)));box-shadow:12px 0 36px rgba(24,31,43,.18)}.left-resizer{display:none}.review-app.sidebar-hidden>.sidebar{display:none}.app-mark>span:not(.revision-badge){display:none}.stage{padding:70px 18px 68px}.floating-zoom{bottom:58px}.canvas-tools{bottom:12px}.canvas-layer-controls{top:50px}}
1091
+ @media(max-width:480px){.version-select{display:none}.screen-context{display:none}.canvas-tools{max-width:calc(100% - 64px);overflow-x:auto}.floating-zoom{right:8px}.menu-bar{padding-inline:7px}.stage{padding-inline:12px}}
1092
+ /* Reduced-command production shell */
1093
+ .inspector-context{min-width:0;display:flex;align-items:center;gap:7px;color:#303947}.inspector-context-icon{width:24px;height:24px;display:grid;place-items:center;border-radius:6px;background:#f0f3f7;color:#526071}.inspector-context-icon .icon{width:14px;height:14px}.inspector-context-title{font-size:10px}.inspector-context-count{min-width:16px;padding:1px 5px;border-radius:999px;background:#eef0f3;color:#687384;font-size:7px;text-align:center}.inspector-context-count[hidden]{display:none}.inspector-pane{padding-top:10px}
1094
+ .header-version-picker{height:28px;max-width:210px;display:flex;align-items:center;gap:5px;padding:0 5px 0 7px;border:1px solid #d9dde3;border-radius:7px;background:#f8f9fb;color:#6a7584}.header-version-picker .icon{width:13px;height:13px;flex:0 0 auto}.header-version-picker .version-select{display:block;min-width:0;width:100%;height:24px;border:0;outline:0;background:transparent;color:#3d4857;padding:0 20px 0 0;font-size:9px;font-weight:650}.review-app[data-view=compare] .header-version-picker{display:none}
1095
+ .canvas-tools .interaction-button{width:30px;padding:0}.review-app[data-view=compare] .interaction-button{width:30px;padding:0}.canvas-tools>.canvas-layer-toggle{height:30px;min-width:35px;padding:0 6px;border-radius:7px}.canvas-tools>.canvas-layer-toggle .canvas-layer-count{margin-left:-2px}.canvas-tools>.canvas-layer-toggle[aria-pressed=true]{background:#e9f0ff;color:#2857ac;box-shadow:none}
1096
+ .compare-workspace-controls{position:absolute;left:50%;top:54px;z-index:19;display:none;align-items:end;gap:6px;transform:translateX(-50%);padding:5px;border:1px solid #d8dce2;border-radius:10px;background:rgba(255,255,255,.97);box-shadow:0 5px 18px rgba(26,32,44,.12);backdrop-filter:blur(10px)}.review-app[data-view=compare] .compare-workspace-controls{display:flex}.compare-version-picker{display:grid;gap:2px}.compare-version-picker>span{padding-left:2px;color:#8a93a2;font-size:7px;font-weight:700;text-transform:uppercase}.compare-version-select{width:146px;height:29px;border:1px solid #d9dde3;border-radius:7px;background:#f8f9fb;color:#35404f;padding:0 24px 0 7px;font-size:8px;font-weight:650}.compare-version-arrow{align-self:center;margin-top:9px;color:#8b94a1;font-size:11px}.compare-workspace-controls .canvas-layer-divider{height:29px;margin-bottom:0}.compare-workspace-controls .compare-layout-controls{min-width:108px;padding:2px}.compare-workspace-controls .compare-button{height:25px}.review-app[data-view=compare] .stage{padding-top:104px}
1097
+ .screen-state-controls{position:absolute;left:50%;top:54px;z-index:19;display:flex;align-items:center;gap:7px;max-width:calc(100% - 96px);transform:translateX(-50%);padding:4px 5px;border:1px solid #d8dce2;border-radius:10px;background:rgba(255,255,255,.97);box-shadow:0 5px 18px rgba(26,32,44,.12);backdrop-filter:blur(10px)}.screen-state-controls[hidden]{display:none}.screen-state-label{padding-left:4px;color:#7a8492;font-size:8px;font-weight:700}.screen-state-switch{display:flex;gap:2px;overflow-x:auto}.screen-state-switch button{height:27px;border:0;border-radius:6px;background:transparent;color:#596575;padding:0 8px;font-size:8px;white-space:nowrap;cursor:pointer}.screen-state-switch button:hover{background:#f0f2f5;color:#273142}.screen-state-switch button[aria-pressed=true]{background:#273142;color:#fff;font-weight:700}.review-app.has-screen-states[data-view=single] .stage,.review-app.has-screen-states[data-view=prototype] .stage{padding-top:104px}.mode-button:disabled{opacity:.35;cursor:not-allowed}.state-gallery{display:grid;grid-template-columns:repeat(2,max-content);align-items:start;gap:28px}.state-panel{display:grid;gap:7px}.state-panel>header{display:flex;align-items:baseline;gap:8px;color:#344054;font-size:11px;font-weight:750}.state-panel>header small{max-width:360px;color:#7a8492;font-size:8px;font-weight:500}.stage.states{justify-content:flex-start;padding-top:62px}.stage.states .device{margin:0}
1098
+ .review-context-actions{grid-template-columns:1fr auto}.review-secondary-action.icon-only{display:none}.menu-spacer{min-width:8px}.workbench-rail .rail-spacer{pointer-events:none}
1099
+ .rail-menu{position:relative;width:32px;height:32px;flex:0 0 auto}.rail-menu>summary.rail-button{width:32px;height:32px;display:grid;place-items:center;padding:0;color:inherit;line-height:0;list-style:none}.rail-menu>summary.rail-button::marker{content:''}.rail-menu>summary.rail-button::-webkit-details-marker{display:none}.rail-menu>summary.rail-button .icon{width:16px;height:16px;margin:0}.rail-menu[open]>summary.rail-button{background:#30394a;color:#fff}.rail-menu .menu-popover{left:38px;right:auto;top:-2px;min-width:176px}.rail-menu.language-menu .menu-popover{left:38px;right:auto;top:-2px}.rail-locale-badge{position:absolute;right:-3px;bottom:-2px;min-width:17px;height:12px;display:grid;place-items:center;padding:0 3px;border:2px solid #171b24;border-radius:999px;background:#e9f0ff;color:#2857ac;font-size:6px;font-weight:800;line-height:1}.workbench-header .menu-bar{gap:0}.workbench-header .screen-context{margin-left:10px}
1100
+ .annotation-pin{position:absolute;width:24px;height:24px;display:grid;place-items:center;transform:translate(-50%,-50%);border:2px solid #fff;border-radius:999px;background:#7c3aed;color:#fff;box-shadow:0 2px 8px rgba(15,23,42,.35);font-size:9px;font-weight:800;cursor:pointer;font-variant-numeric:tabular-nums;pointer-events:auto;touch-action:manipulation}.annotation-pin:hover,.annotation-pin:focus-visible{transform:translate(-50%,-50%) scale(1.12);outline:3px solid rgba(196,181,253,.9);outline-offset:2px}.annotation-pin-card{border-color:#c4b5fd}.annotation-pin-number{background:#7c3aed}.finding-pin-leader:has(+ .annotation-pin:hover),.finding-pin-leader:has(+ .annotation-pin:focus-visible),.finding-pin-leader:has(+ .annotation-pin-card){background:rgba(124,58,237,.58);opacity:1}
1101
+ @container(max-width:760px){.header-version-picker{max-width:150px}.canvas-tools .interaction-button{width:30px}.review-app[data-view=compare] .interaction-button{width:30px}.compare-version-select{width:112px}.compare-workspace-controls{max-width:calc(100% - 24px)}}
1102
+ @media(max-width:720px){.compare-workspace-controls{top:50px}.compare-version-picker>span,.compare-version-arrow{display:none}.compare-version-select{width:104px}.compare-workspace-controls .compare-layout-controls{min-width:94px}.review-app[data-view=compare] .stage{padding-top:92px}}
1103
+ /* Clear scenario switch and review-selection handoff */
1104
+ .screen-state-controls{top:56px;gap:6px;max-width:calc(100% - 112px);min-height:38px;padding:4px 5px;border-color:#d9dee7;border-radius:9px;background:#f7f8fa;box-shadow:0 3px 10px rgba(30,41,59,.10);backdrop-filter:none}.screen-state-label{flex:0 0 auto;padding:0;color:#697587;font-size:8px;font-weight:700}.screen-control-group{display:flex;align-items:center;gap:6px;min-width:0}.screen-control-group+.screen-control-group{margin-left:2px;padding-left:8px;border-left:1px solid #d9dee7}.screen-state-select{width:142px;min-width:0;height:30px;border:1px solid #d3d9e2;border-radius:7px;background:#fff;color:#344154;padding:0 28px 0 9px;font-size:9px;font-weight:650;cursor:pointer;text-overflow:ellipsis}.screen-control-group.scenario .screen-state-select{width:184px}.screen-state-select:hover{border-color:#aeb9c8}.screen-state-select:focus-visible{border-color:#315fbd;outline:2px solid #a8c0ed;outline-offset:1px}.open-variants{display:inline-flex;align-items:center;gap:5px;height:30px;margin-left:2px;padding:0 9px;border:0;border-left:1px solid #d9dee7;background:transparent;color:#3e4c5f;font-size:9px;font-weight:700;white-space:nowrap;cursor:pointer}.open-variants:hover{border-radius:6px;background:#fff;color:#234f9f}.open-variants:focus-visible{outline:2px solid #8eb0f1;outline-offset:1px}.open-variants svg{width:13px;height:13px}.review-app.has-screen-states[data-view=single] .stage,.review-app.has-screen-states[data-view=prototype] .stage{padding-top:108px}.variant-gallery{display:grid;gap:18px;min-width:max-content;transform-origin:top left}.variant-gallery-toolbar{position:sticky;left:0;z-index:3;display:flex;align-items:center;justify-content:space-between;gap:18px;width:min(920px,calc(100vw - 180px));min-width:680px;padding:9px 10px;border:1px solid #d8dee8;border-radius:10px;background:rgba(255,255,255,.96);box-shadow:0 5px 18px rgba(30,41,59,.09);backdrop-filter:blur(8px)}.variant-gallery-title{display:flex;align-items:baseline;gap:9px;min-width:120px}.variant-gallery-toolbar b{font-size:11px;color:#253044}.variant-gallery-toolbar span{font-size:9px;color:#748094}.variant-gallery-actions,.variant-dimension-controls{display:flex;align-items:center;gap:6px}.variant-dimension-controls .screen-state-select{width:118px}.variant-dimension-controls .screen-control-group.scenario .screen-state-select{width:150px}.variant-dimension-controls .screen-control-group+.screen-control-group{margin-left:0;padding-left:0;border-left:0}.variant-axis-switch{display:flex;flex:0 0 auto;gap:2px;padding:2px;border-radius:7px;background:#eef1f5}.variant-axis-switch button{height:27px;padding:0 10px;border:0;border-radius:5px;background:transparent;color:#536174;font-size:9px;font-weight:650;cursor:pointer}.variant-axis-switch button[aria-pressed=true]{background:#fff;color:#204b97;box-shadow:0 1px 3px rgba(30,41,59,.15)}.variant-sections{display:grid;gap:26px}.variant-section{display:grid;gap:11px;padding:14px;border:1px solid #d9dfe8;border-radius:12px;background:rgba(246,248,251,.78)}.variant-section>header{position:sticky;left:14px;display:flex;align-items:baseline;gap:9px;width:max-content;color:#344054}.variant-section>header b{font-size:12px}.variant-section>header span{font-size:9px;color:#7b8798}.variant-card{padding:9px;border:1px solid transparent;border-radius:10px;background:#fff;box-shadow:0 5px 16px rgba(30,41,59,.08)}.variant-card[data-current-variant=true]{border-color:#7fa2e2;box-shadow:0 0 0 2px rgba(49,95,189,.13),0 5px 16px rgba(30,41,59,.08)}.variant-card>header{min-height:28px;padding:0 2px}.stage.states{align-items:flex-start}.stage.states .state-gallery{gap:18px}@media(max-width:900px){.screen-state-label{display:none}.screen-state-select{width:118px}.screen-control-group.scenario .screen-state-select{width:148px}.open-variants span{display:none}.state-gallery{grid-template-columns:max-content}.variant-gallery-toolbar{align-items:flex-start;min-width:520px}.variant-gallery-actions{align-items:flex-end;flex-direction:column}.variant-dimension-controls .screen-state-label{display:block}}
1105
+ .variant-gallery{transform-origin:initial}.variant-canvas{min-width:max-content;transform-origin:top left}
1106
+ .finding-selection-count{grid-column:1/-1;min-height:25px;display:flex;align-items:center;justify-content:center;border-radius:7px;background:#f4f6f8;color:#657181;font-size:8px;font-weight:700}.finding-selection-count.has-selection{background:#eaf1ff;color:#174ea6}
1107
+ .version-application-status{max-width:168px;overflow:hidden;padding:4px 7px;border-radius:999px;background:#fff4d6;color:#805b12;font-size:8px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.version-application-status[data-state=prepared]{background:#eaf1ff;color:#174ea6}.version-application-status[data-state=verified]{background:#e6f5eb;color:#25613d}.version-application-status[hidden],.review-secondary-action[hidden]{display:none!important}.problem-overview.addressed{background:#eef4ff;color:#315fbd}.problem-overview.addressed b{color:#174ea6}.finding-card-status.addressed{background:#eaf1ff;color:#174ea6}.finding-bulk button:disabled{opacity:.48;cursor:not-allowed}
1108
+ /* Review text must stay readable; metadata may be compact, task text may not. */
1109
+ .review-subtab{height:34px;font-size:11px}.review-subtab-count{font-size:9px}.review-scope-label,.review-scope-select,.finding-select{font-size:10px}.coverage-head h3,.history-head h3,.import-review-head h3{font-size:12px}.coverage-metric span,.coverage-row,.review-history-item,.review-history-item small,.coverage-gaps,.import-review-copy,.import-review-status{font-size:9px}.history-clear,.finding-group-label{font-size:9px}.problem-overview b{font-size:12px}.problem-overview span,.audit-details>summary{font-size:10px}.finding-selection-count{grid-column:auto;justify-content:flex-start;padding:0 9px;font-size:10px}.finding-bulk{grid-template-columns:minmax(0,1fr) auto}.finding-bulk button,.import-review-action{min-height:32px;font-size:10px}.finding-card-status{font-size:9px}.finding-title{font-size:11px}.finding-observation,.finding-impact,.finding-recommendation{font-size:11px;line-height:1.45}.finding-details>summary,.finding-evidence>summary,.finding-meta,.finding-origin{font-size:9px}.finding-action,.finding-more>summary{min-height:30px;font-size:10px}.review-context-status{font-size:10px}.review-next-action,.review-secondary-action{min-height:38px;font-size:11px}.version-application-status{font-size:9px}
1110
+ @media(max-width:980px){.review-app:not(.inspector-hidden) .canvas-tools{left:calc((100% - 360px)/2)}.review-app:not(.inspector-hidden) .floating-zoom{right:374px;bottom:62px}}
1111
+ @media(max-width:620px){.review-app:not(.inspector-hidden) .canvas-tools,.review-app:not(.inspector-hidden) .floating-zoom{display:none}.inspector{width:calc(100vw - var(--rail))}}
1112
+ """
1113
+
1114
+ JS = r"""
1115
+ const previewContext=JSON.parse(document.getElementById('ui-preview-context')?.textContent||'{}');const diagnosticsEngineVersion=Number(previewContext.diagnosticsEngineVersion||1),interactionRules=previewContext.interactionContract||{version:1,nativeNodeTypes:['button','input'],semanticRoles:[],actionTypes:[]},interactiveNodeTypes=new Set(interactionRules.nativeNodeTypes||[]),interactiveRoles=new Set(interactionRules.semanticRoles||[]),interactiveActionTypes=new Set(interactionRules.actionTypes||[]);function nodeInteraction(node){const nodeType=String(node?.type||'').trim().toLowerCase(),actionType=String(node?.action?.type||'').trim(),semanticRole=String(node?.semantics?.role||'').trim().toLowerCase(),sources=[];if(interactiveNodeTypes.has(nodeType))sources.push('native-node');if(interactiveActionTypes.has(actionType))sources.push('action');if(interactiveRoles.has(semanticRole))sources.push('semantic-role');return {version:interactionRules.version||1,interactive:sources.length>0,sources,nodeType,actionType,semanticRole,keyboardExpected:sources.length>0}}const ir=JSON.parse(document.getElementById('ui-ir-data').textContent);const nodes=ir.nodes||{},screens=ir.screens||[],navigationGraph=ir.navigationGraph&&typeof ir.navigationGraph==='object'?ir.navigationGraph:{},scenarioFixtures=ir.scenarioFixtures||{},review=ir.review||{},expertAudit=review.audit||{};const themeConfig=ir.themes&&typeof ir.themes==='object'?ir.themes:{},themes=Array.isArray(themeConfig.items)&&themeConfig.items.length?themeConfig.items:[{id:'light',label:'Light',kind:'light',tokenOverrides:{},nodeOverrides:{}}],themeById=Object.fromEntries(themes.map(theme=>[theme.id,theme])),defaultThemeId=themeConfig.defaultThemeId&&themeById[themeConfig.defaultThemeId]?themeConfig.defaultThemeId:themes[0].id;const baseFindings=Array.isArray(expertAudit.findings)?expertAudit.findings:[];const diagnosticsConfig=review.diagnostics||{profiles:[{id:'current',label:'Текущее окно',viewport:'current',zoomLevels:[.2,1,2]}],scenarios:[{id:'screen-render',label:'Перенос экранов',kind:'screen-render'},{id:'zoom-reset',label:'Сброс масштаба',kind:'zoom-reset'},{id:'overview-geometry',label:'Геометрия обзора',kind:'overview-geometry'},{id:'menu-exclusivity',label:'Взаимодействие меню',kind:'menu-exclusivity'},{id:'layout-integrity',label:'Целостность макета',kind:'layout-integrity'},{id:'accessibility-basics',label:'Базовая доступность',kind:'accessibility-basics'}]};
1116
+ const mandatoryDiagnosticScenarios=[{id:'screen-render',label:'Перенос экранов',kind:'screen-render'},{id:'state-matrix',label:'Варианты тем и состояний',kind:'state-matrix'},{id:'navigation-flow',label:'Пользовательские переходы',kind:'navigation-flow'},{id:'contrast-focus',label:'Контраст и клавиатура',kind:'contrast-focus'}],projectionDiagnosticKinds=new Set(['screen-render','zoom-reset','overview-geometry','menu-exclusivity','state-matrix','navigation-flow']);diagnosticsConfig.profiles=diagnosticsConfig.profiles||[];diagnosticsConfig.scenarios=diagnosticsConfig.scenarios||[];for(const scenario of mandatoryDiagnosticScenarios)if(!diagnosticsConfig.scenarios.some(item=>item.kind===scenario.kind))diagnosticsConfig.scenarios.push(scenario);
1117
+ const supportedWorkbenchLocales=['ru','en'],workbenchLocaleStorageKey='ui-design-workbench:locale';function normalizeWorkbenchLocale(value){const normalized=String(value||'').toLowerCase().split('-')[0];return supportedWorkbenchLocales.includes(normalized)?normalized:null}const locationLocale=normalizeWorkbenchLocale(new URL(location.href).searchParams.get('lang'));let storedLocale=null;try{storedLocale=normalizeWorkbenchLocale(localStorage.getItem(workbenchLocaleStorageKey))}catch(_){}let workbenchLocale=locationLocale||storedLocale||normalizeWorkbenchLocale(navigator.language)||'ru';document.documentElement.lang=workbenchLocale;
1118
+ const versions=Array.isArray(review.versions)&&review.versions.length?review.versions:[{id:'baseline',label:'Before',kind:'baseline',status:'approved',nodeOverrides:{}}];
1119
+ const versionById=Object.fromEntries(versions.map(v=>[v.id,v]));const baselineVersion=review.baselineVersion&&versionById[review.baselineVersion]?review.baselineVersion:versions[0].id;const redesignDefaultVersion=ir.design?.mode==='redesign'?[...versions].reverse().find(version=>version.kind==='proposal')?.id:null;const initialVersion=redesignDefaultVersion||(review.activeVersion&&versionById[review.activeVersion]?review.activeVersion:versions.at(-1).id);const proposalVersions=()=>versions.filter(version=>version.kind==='proposal');
1120
+ function revisionHash(value){let hash=2166136261;for(let index=0;index<value.length;index++){hash^=value.charCodeAt(index);hash=Math.imul(hash,16777619)}return (hash>>>0).toString(36)}
1121
+ const workbenchSchema=9,reviewBaseRevision=review.revision||'auto',reviewTargetRevision=`${reviewBaseRevision}-${revisionHash(JSON.stringify({screens,nodes,themes,versions,audit:expertAudit}))}`,reviewRevision=reviewTargetRevision;const storageKey=`ui-design-workbench:${ir.project?.name||'project'}:${review.sessionId||'default'}`,legacyStorageKey=`ui-code-preview:${ir.project?.name||'project'}:${review.sessionId||'default'}`,compatibleStorageKeys=[storageKey,legacyStorageKey,...(Array.isArray(review.legacyStorageKeys)?review.legacyStorageKeys:[])];let saved={};try{saved=JSON.parse(compatibleStorageKeys.map(key=>localStorage.getItem(key)).find(Boolean)||'{}')}catch(_){saved={}}const savedTargetRevision=saved.targetRevision||saved.revision||null,legacySameReview=Boolean(saved.revision&&!saved.targetRevision&&String(saved.revision).startsWith(`${reviewBaseRevision}-`)),autoMigratedReviewState=Boolean(legacySameReview&&savedTargetRevision!==reviewTargetRevision);if(autoMigratedReviewState)saved={...saved,workbenchSchema,targetRevision:reviewTargetRevision,revision:reviewRevision};const staleReviewSnapshot=saved.revision&&savedTargetRevision!==reviewTargetRevision&&!autoMigratedReviewState?saved:null;if(staleReviewSnapshot)saved={};
1122
+ const annotationMap=new Map((review.annotations||[]).map(item=>[item.id,item]));for(const item of saved.annotations||[])annotationMap.set(item.id,item);
1123
+ function isRuntimeFinding(item){return item?.category==='runtime-diagnostic'||Boolean(item?.runtimeDiagnosticId)||Boolean(item?.runtimeDiagnosticIds)}const savedDiagnosticsVersion=Number(saved.diagnosticsEngineVersion||saved.diagnostics?.engineVersion||0),savedDiagnosticsCompatible=savedDiagnosticsVersion===diagnosticsEngineVersion,savedHasExplicitReview=savedDiagnosticsCompatible&&saved.diagnostics?.mode==='review',savedReviewFindings=Array.isArray(saved.runtimeFindings)?saved.runtimeFindings:[],compatibleSavedFindings=savedReviewFindings.filter(item=>savedDiagnosticsCompatible||!isRuntimeFinding(item)),compatibleDecisionIds=new Set([...baseFindings,...compatibleSavedFindings].map(item=>item?.id).filter(Boolean)),savedDecisions=Object.fromEntries(Object.entries(saved.findingDecisions||expertAudit.findingDecisions||{}).filter(([id])=>compatibleDecisionIds.has(id)));const state={interaction:'interact',view:'overview',screen:null,selected:null,selectedNodeId:saved.selectedNodeId||null,selectedScreenId:saved.selectedScreenId||null,focusedFindingId:saved.focusedFindingId||null,openFindingId:saved.openFindingId||null,showFindings:saved.showFindings!==false,showBefore:redesignDefaultVersion?false:(typeof saved.showBefore==='boolean'?saved.showBefore:initialVersion===baselineVersion),showAfter:redesignDefaultVersion?true:(typeof saved.showAfter==='boolean'?saved.showAfter:initialVersion!==baselineVersion),layerReturnView:['overview','prototype','single','states'].includes(saved.layerReturnView)?saved.layerReturnView:'single',nodeStates:{},screenScenarioIds:saved.screenScenarioIds||{},activeThemeId:saved.activeThemeId&&themeById[saved.activeThemeId]?saved.activeThemeId:defaultThemeId,variantAxis:['themes','states','matrix'].includes(saved.variantAxis)?saved.variantAxis:'states',screenScrolls:saved.screenScrolls||{},stageScroll:saved.stageScroll||{left:0,top:0},activeVersion:redesignDefaultVersion||(saved.activeVersion&&versionById[saved.activeVersion]?saved.activeVersion:initialVersion),compareMode:saved.compareMode||'split',overlayPosition:saved.overlayPosition||50,zoomMode:saved.zoomMode||'fit',zoom:Number(saved.zoom)||1,computedZoom:1,sidebarOpen:saved.sidebarOpen!==false,inspectorOpen:saved.inspectorOpen!==false,inspectorTab:['inspect','review','comments'].includes(saved.inspectorTab)?saved.inspectorTab:'review',reviewSection:['summary','problems','changes'].includes(saved.reviewSection)?saved.reviewSection:'summary',reviewScope:['all','current'].includes(saved.reviewScope)?saved.reviewScope:'all',leftWidth:Number(saved.leftWidth)||244,rightWidth:Number(saved.rightWidth)||324,findingFilter:saved.findingFilter||'all',findingSource:saved.findingSource||'all',findingScreen:saved.findingScreen||'all',findingFocus:Array.isArray(saved.findingFocus)?saved.findingFocus.filter(id=>compatibleDecisionIds.has(id)):[],findingDecisions:savedDecisions,runtimeFindings:compatibleSavedFindings,annotations:[...annotationMap.values()],versionDecision:saved.versionDecision||'pending',sourcePreparedVersionId:saved.sourcePreparedVersionId||null,diagnostics:savedHasExplicitReview?saved.diagnostics:null,diagnosticsRunning:false,diagnosticsProgress:'',reviewRuns:Array.isArray(saved.reviewRuns)?saved.reviewRuns:[],importedReview:saved.importedReview||null,agentHandoff:saved.agentHandoff||saved.codexHandoff||null,diagnosticTargetIds:savedHasExplicitReview&&Array.isArray(saved.diagnosticTargetIds)?saved.diagnosticTargetIds:[],staleReview:staleReviewSnapshot};
1124
+ state.openAnnotationId=null;state.compareBaseVersion=saved.compareBaseVersion&&versionById[saved.compareBaseVersion]?saved.compareBaseVersion:baselineVersion;state.compareTargetVersion=redesignDefaultVersion||(saved.compareTargetVersion&&versionById[saved.compareTargetVersion]?saved.compareTargetVersion:state.activeVersion);state.reviewSessionName=saved.reviewSessionName||review.sessionId||ir.project?.name||'Ревью';
1125
+ const findings=new Proxy([],{get(_,property){const items=[...baseFindings,...state.runtimeFindings];const value=items[property];return typeof value==='function'?value.bind(items):value}});
1126
+ const $=s=>document.querySelector(s);const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));const iconMarkup=name=>`<svg class="icon" aria-hidden="true"><use href="#icon-${esc(name)}"></use></svg>`;
1127
+ function versionRoleLabel(version){return version?.id===baselineVersion||version?.kind==='baseline'?'Исходный макет':'Новый макет'}
1128
+ function versionDisplayLabel(version){const role=versionRoleLabel(version),raw=String(version?.label||version?.id||'').trim(),detail=raw.replace(/^(before|after|до|после)\s*(?:[·:—-]\s*)?/i,'').trim();return detail&&detail!==role?`${role} · ${detail}`:role}
1129
+ function renderVersionStatus(){const badge=$('.version-application-status'),version=versionById[state.activeVersion];if(!badge)return;const proposal=version?.kind==='proposal';badge.hidden=!proposal;if(!proposal)return;const verified=findings.length>0&&findings.every(findingResolved),implementationPrepared=state.agentHandoff?.kind==='implement'&&state.agentHandoff.status==='prepared';badge.textContent=verified?'Применён и проверен':implementationPrepared?'Задание на применение подготовлено':'Ещё не применён к проекту';badge.dataset.state=verified?'verified':implementationPrepared?'prepared':'preview'}
1130
+ const workbenchTranslations={en:{
1131
+ "Показываемая версия":"Displayed version","Показать обе версии":"Show both versions","До":"Before","Сравнение":"Compare","После":"After","Настройки сравнения":"Compare settings","Слева":"Left","Справа":"Right","Версия слева":"Left version","Версия справа":"Right version","Холст UI Design Workbench":"UI Design Workbench canvas","Наложение версий До и После":"Before and after overlay","Колесо мыши масштабирует холст; зажатое колёсико перемещает холст":"Mouse wheel zooms the canvas; hold the middle mouse button to pan",
1132
+ "Текущее окно":"Current window","Перенос экранов":"Screen projection","Сброс масштаба":"Reset zoom","Геометрия обзора":"Overview geometry","Взаимодействие меню":"Menu interaction","Целостность макета":"Layout integrity","Базовая доступность":"Basic accessibility","Состояния компонентов":"Component states","Варианты тем и состояний":"Theme and state variants","Пользовательские переходы":"User flows","Контраст и клавиатура":"Contrast and keyboard",
1133
+ "Разделы рабочего пространства":"Workspace sections","Экраны":"Screens","Свойства выбранного элемента":"Selected element properties","Свойства":"Properties","Ревью и исправления":"Review and fixes","Ревью":"Review","Комментарии":"Comments","Скрыть или показать правую панель":"Toggle right panel","Правая панель":"Right panel","Навигация":"Navigation","Скрыть панель экранов":"Hide screens panel","Скрыть панель":"Hide panel","Найти экран":"Find screen","Иерархия экранов":"Screen hierarchy","Изменить ширину дерева экранов":"Resize screen tree","Ревизия макета":"Mockup revision","Главное меню":"Main menu","Файл":"File","Экспорт ревью":"Export review","Копировать JSON":"Copy JSON","Вид":"View","Все экраны":"All screens","Прототип":"Prototype","Один экран":"Single screen","Сравнение версий":"Compare versions","Дерево экранов":"Screen tree","Инспектор и ревью":"Inspector and review","Вписать макет в окно":"Fit mockup to window","Подготовить исправления…":"Prepare fixes…","Диагностика макета…":"Mockup diagnostics…","Принять версию":"Approve version","Запросить изменения":"Request changes","Экспорт комментариев":"Export comments","Язык интерфейса":"Interface language","Активная версия макета":"Active mockup version",
1134
+ "Макеты · переходы выключены":"Mockups · navigation off","Инструменты холста":"Canvas tools","Режим просмотра":"View mode","Макеты":"Mockups","Интерактивный прототип":"Interactive prototype","Экран":"Screen","Состояния экрана":"Screen states","Все варианты текущего экрана":"All variants of the current screen","Варианты экрана":"Screen variants","Варианты":"Variants","Темы":"Themes","Тема":"Theme","По темам":"By theme","По состояниям":"By state","Матрица":"Matrix","Светлая":"Light","Тёмная":"Dark","Разложить на холсте":"Lay out on canvas","Состояния":"States","Состояние":"State","Данные":"Data","Сравнить":"Compare","Инструмент":"Tool","Взаимодействовать с макетом":"Interact with mockup","Взаимодействовать":"Interact","Клики":"Clicks","Инспектировать элемент":"Inspect element","Инспектировать":"Inspect","Добавить комментарий":"Add comment","Комментарий":"Comment","Видимость элементов на макете":"Mockup layers","Показывать проблемы на макете":"Show issues on mockup","Показать или скрыть проблемы":"Show or hide issues","Проблемы":"Issues","Показать исходную версию":"Show original version","Показать предложенную версию":"Show proposed version","Способ сравнения":"Comparison layout","Рядом":"Side by side","Наложение":"Overlay","Масштаб макета":"Mockup zoom","Колесо мыши масштабирует холст; двойной клик по разделителю сбрасывает ширину панели":"Mouse wheel zooms the canvas; double-clicking a divider resets panel width","Уменьшить масштаб":"Zoom out","Уменьшить":"Zoom out","Масштаб 100%":"Zoom 100%","Сбросить масштаб":"Reset zoom","Увеличить масштаб":"Zoom in","Увеличить":"Zoom in","Вписать макет":"Fit mockup","Изменить ширину панели ревью":"Resize review panel",
1135
+ "Скрыть правую панель":"Hide right panel","Выбранный элемент":"Selected element","Выберите элемент":"Select an element","Переключитесь на «Свойства» и нажмите элемент на холсте.":"Choose Properties and click an element on the canvas.","Разделы ревью":"Review sections","Этапы ревью":"Review steps","Сводка":"Summary","Проверка":"Check","Найти проблемы":"Find issues","найдено":"found","Исправление":"Fix","выбрано":"selected","Изменения":"Changes","Доступна новая ревизия":"A new revision is available","Перенесите совместимые решения или начните чистое ревью.":"Migrate compatible decisions or start a clean review.","Перенести":"Migrate","Сбросить":"Reset","Запуск ревью макетов":"Start mockup review","Ревью макетов":"Mockup review","Область проверки":"Review scope","Область ревью":"Review scope","Текущий экран":"Current screen","Запустить ревью":"Run review","Дополнительно":"More options","Углублённый анализ":"In-depth analysis","Скачать технический файл":"Download technical file","Что проверено":"What was checked","Подробности проверки":"Check details","Предыдущие проверки":"Previous checks","История проверок":"Check history","Название проверки":"Check name","Подготовить AI-ревью":"Prepare AI review","Скачать JSON-задание":"Download JSON job","Покрытие ревью":"Review coverage","Покрытие":"Coverage","История ревью":"Review history","История запусков":"Run history","Очистить":"Clear","Название сессии ревью":"Review session name","Название сессии":"Session name","Технические результаты":"Technical results","не запускалась":"not run","Запустить":"Run",
1136
+ "Источник проблемы":"Issue source","Все источники":"All sources","Экспертный аудит":"Expert audit","Автопроверка":"Automated check","Системные":"Systemic","Фильтр замечаний":"Issue filter","Фильтры":"Filters","Все":"All","Выбрано":"Selected","Блокеры":"Blockers","Критичные":"Critical","Важные":"Important","Обычные":"Normal","Высокие":"High","Средние":"Medium","Низкие":"Low","Выбрать показанные":"Select visible","Выбрать все":"Select all","Исправить всё в проекте":"Fix everything in project","Исправить всё сразу в проекте":"Fix all directly in project","Исправить без предварительного просмотра":"Fix without preview","Этот путь сразу подготовит изменение файлов проекта.":"This path immediately prepares project file changes.","Снять выбор":"Clear selection","План исправлений":"Fix plan","Найдено":"Found","Исправлено в макете":"Fixed in mockup","Подтверждено":"Approved","Применение":"Implementation","Проверено":"Verified","Скопировать задание":"Copy job","Скопировать запрос":"Copy request","Обновить после исправления":"Refresh after implementation","Результат AI-агента":"AI agent result","Технические действия":"Technical actions","Обмен с внешним агентом":"External agent exchange","Используйте только если исправление выполняется в другом инструменте.":"Use only when the fix is performed in another tool.","не импортирован":"not imported","Агент может пересобрать файлы макета напрямую. Импорт JSON остаётся переносимым резервным способом.":"The agent can rebuild mockup files directly. JSON import remains a portable fallback.","Импортировать JSON":"Import JSON","Скачать задание":"Download job","Ожидает решения":"Awaiting decision","На доработку":"Request changes","Экспортировать ревью":"Export review","Экспортировать":"Export","Обратная связь":"Feedback","Комментарий будет привязан к выбранному узлу.":"The comment will be attached to the selected node.",
1137
+ "Открыть":"Open","Нет экранов":"No screens","Переходы выключены. Выберите режим «Прототип».":"Navigation is off. Choose Prototype mode.","Скрыть панель экранов":"Hide screens panel","Показать панель экранов":"Show screens panel","Показать правую панель":"Show right panel","Для сравнения пока нет версии «После»":"There is no After version to compare yet","На холсте должна остаться хотя бы одна версия":"At least one version must remain visible","Свернуть описание проблемы":"Collapse issue description","Свернуть комментарий":"Collapse comment","Свернуть в маркер":"Collapse to marker","Исправить:":"Fix:","В списке проблем":"Open in issue list","В списке комментариев":"Open in comment list","Комментарий добавлен":"Comment added","Что нужно изменить?":"What should change?","Средний приоритет":"Medium priority","Высокий приоритет":"High priority","Низкий приоритет":"Low priority","Добавить":"Add","Узел":"Node","Версия":"Version","Компонент":"Component","Каталог":"Catalog","Источник":"Source","Символ":"Symbol","Стандарт":"Standard","Решение":"Decision","Семантика":"Semantics","Цель":"Target","Точность":"Confidence","Свойства, источник и стандарт появятся здесь.":"Properties, source, and standard will appear here.",
1138
+ "проверяется":"running","готово":"complete","не запускалось":"not run","Покрытие станет измеримым после запуска.":"Coverage becomes measurable after a run.","Экранов":"Screens","Тесты":"Tests","Сост.":"States","Пробл.":"Issues","Запусков пока нет":"No runs yet","История появится после первой проверки.":"History appears after the first run.","Проблем пока нет.":"No issues yet.","Не выбрано":"Not selected","В исправление":"Add to fixes","Не исправлять":"Do not fix","Отложено":"Deferred","Позже":"Later","Влияние и решение":"Impact and solution","Влияние:":"Impact:","Изменить:":"Change:","Вариант готов для сравнения.":"A proposal is ready to compare.","Требуется новый вариант макета.":"A new mockup proposal is required.","Показать":"Show","Не исправлять":"Do not fix","Здесь пока пусто":"Nothing here yet","Измените фильтр или выберите другой блок аудита.":"Change the filter or choose another audit group.","Эксперт":"Expert","Системная":"Systemic","Выбранный блок":"Focused group","Сбросить":"Reset",
1139
+ "Проблемы исходной версии сохранены как проверяемая история ревью.":"Baseline issues are preserved as reviewable history.","Все найденные проблемы доступны в списке ниже.":"All detected issues are available in the list below.","Создать вариант макета":"Create mockup proposal","Подготовить внедрение в проект":"Prepare project implementation","Версия отмечена как принята":"Version marked as approved","Версия отправлена на доработку":"Version sent for revision","JSON скопирован":"JSON copied","Результаты диагностики очищены":"Diagnostic results cleared","История запусков очищена":"Run history cleared","Проверка завершена":"Review complete","Подготовка сценариев…":"Preparing scenarios…",
1140
+ "Исправление проблем":"Issue resolution","Выбрать проблемы":"Choose issues","Отдельные пункты или всё сразу":"Individual items or everything at once","Проверить решение":"Review the solution","Посмотреть макет «После» или пропустить этот шаг":"Inspect the After mockup or skip this step","Применить в проекте":"Apply to project","Изменить реальные файлы и выполнить целевые проверки":"Change real files and run targeted checks"
1141
+ }};
1142
+ Object.assign(workbenchTranslations.en,{"Ничего не выбрано":"Nothing selected","Выбрать проблемы":"Choose issues","Подтвердить результат":"Approve result","Проверить результат":"Check result","Исходный макет":"Original mockup","Новый макет":"New mockup","Исходный":"Original","Новый":"New","Какой макет сейчас показан":"Currently displayed mockup","Макет слева для сравнения":"Left mockup to compare","Макет справа для сравнения":"Right mockup to compare","Наложение исходного и нового макета":"Original and new mockup overlay","Проверить новый макет":"Review new mockup","Сравнить его с исходным макетом":"Compare it with the original mockup"});
1143
+ Object.assign(workbenchTranslations.en,{"Сохранить ревью":"Save review","Восстановить ревью":"Restore review","Копировать резервную копию":"Copy backup","Ревью сохранено в файл":"Review saved to file","Ревью восстановлено":"Review restored","Ревью восстановлено; техническая проверка будет выполнена заново":"Review restored; technical checks will run again"});
1144
+ Object.assign(workbenchTranslations.en,{"Вариант данных экрана":"Screen data variant","Ещё не применён к проекту":"Not applied to project yet","Задание на применение подготовлено":"Application task prepared","Применён и проверен":"Applied and verified","Учтено в новом макете":"Addressed in new mockup","Применено и проверено":"Applied and verified","Выбрано для применения":"Selected to apply","Показать в исходном":"Show in original","Посмотреть исправление":"Review fix","Выбрать для применения":"Select to apply","Все выбрано":"All selected","Новый макет ещё не подтверждён":"New mockup is not approved yet","Новый макет подтверждён · в проект не применён":"New mockup approved · not applied to project","Новый макет требует доработки":"New mockup needs changes","Обновить после применения":"Refresh after application","Подтвердить новый макет":"Approve new mockup"});
1145
+ Object.assign(workbenchTranslations.en,{"Предупреждения":"Warnings","Режим:":"Mode:","Связь с исходниками:":"Source mapping:","Основания решений:":"Decision evidence:","Оформление:":"Appearance:","Высокая уверенность:":"High confidence:","Компоненты:":"Components:","Стандарты:":"Standards:","Семантика:":"Semantics:","Размер целей:":"Target size:","Контраст:":"Contrast:","Состояния:":"States:","Экраны:":"Screens:","Маршруты:":"Routes:","Цели меню:":"Menu targets:","Переходы:":"Navigation actions:","Сбросить фильтр":"Clear filter"});
1146
+ const localizedTextState=new WeakMap(),localizedAttributeState=new WeakMap();
1147
+ function translateWorkbenchSource(source){if(workbenchLocale==='ru')return source;const exact=workbenchTranslations[workbenchLocale]?.[source];if(exact)return exact;const patterns=[[/^(Исходный макет|Новый макет) · (.+)$/,(_,kind,label)=>`${kind==='Исходный макет'?'Original mockup':'New mockup'} · ${label}`],[/^Все экраны · (\d+)$/,(_,count)=>`All screens · ${count}`],[/^Замечаний: (\d+)$/,(_,count)=>`Issues: ${count}`],[/^Откроется: (.+)$/,(_,path)=>`Opens: ${path}`],[/^Выбранный блок · (\d+)$/,(_,count)=>`Focused group · ${count}`],[/^Выбрано: (\d+)$/,(_,count)=>`Selected: ${count}`],[/^К выбранным проблемам · (\d+)$/,(_,count)=>`Selected issues · ${count}`],[/^Перейти к исправлению · (\d+)$/,(_,count)=>`Continue to fixes · ${count}`],[/^Подготовить исправления · (\d+)$/,(_,count)=>`Prepare fixes · ${count}`],[/^Проверить исправления · (\d+)$/,(_,count)=>`Review fixes · ${count}`],[/^Подготовить новый макет · (\d+)$/,(_,count)=>`Prepare new mockup · ${count}`],[/^Проверить новый макет · (\d+)$/,(_,count)=>`Review new mockup · ${count}`],[/^Сравнить с исходным · (\d+)$/,(_,count)=>`Compare with original · ${count}`],[/^Применить исправления · (\d+)$/,(_,count)=>`Apply fixes · ${count}`],[/^Проверка: (.+)$/,(_,name)=>`Checking: ${translateWorkbenchSource(name)}`],[/^Диагностика завершена: ошибок (\d+), предупреждений (\d+)$/,(_,fails,warnings)=>`Diagnostics complete: ${fails} failures, ${warnings} warnings`],[/^(\d+) проблем$/,(_,count)=>`${count} issues`],[/^(\d+) проверок$/,(_,count)=>`${count} checks`],[/^(\d+) состояний$/,(_,count)=>`${count} states`]];for(const [pattern,format] of patterns){const match=source.match(pattern);if(match)return format(...match)}return source}
1148
+ const translateWorkbenchSourceDefault=translateWorkbenchSource;
1149
+ translateWorkbenchSource=function(source){if(workbenchLocale==='en'){let match=source.match(/^Показана (\d+) из (\d+) проблем$/);if(match)return `Showing ${match[1]} of ${match[2]} issues`;match=source.match(/^Посмотреть проблемы · (\d+)$/);if(match)return `View issues · ${match[1]}`;match=source.match(/^Выбрать все · (\d+)$/);if(match)return `Select all · ${match[1]}`}return translateWorkbenchSourceDefault(source)};
1150
+ function i18nExcluded(element){return !element||Boolean(element.closest?.('.device,[data-i18n-skip]'))}
1151
+ function localizeTextNode(node){const parent=node.parentElement;if(i18nExcluded(parent))return;const current=node.nodeValue||'',trimmed=current.trim();if(!trimmed)return;let record=localizedTextState.get(node);if(!record||current!==record.rendered){const leading=current.match(/^\s*/)?.[0]||'',trailing=current.match(/\s*$/)?.[0]||'';record={source:trimmed,leading,trailing,rendered:current}}const translated=`${record.leading}${translateWorkbenchSource(record.source)}${record.trailing}`;record.rendered=translated;localizedTextState.set(node,record);if(current!==translated)node.nodeValue=translated}
1152
+ function localizeAttribute(element,name){if(i18nExcluded(element)||!element.hasAttribute(name))return;const current=element.getAttribute(name)||'';if(!current)return;let records=localizedAttributeState.get(element);if(!records){records={};localizedAttributeState.set(element,records)}let record=records[name];if(!record||current!==record.rendered)record=records[name]={source:current,rendered:current};const translated=translateWorkbenchSource(record.source);record.rendered=translated;if(current!==translated)element.setAttribute(name,translated)}
1153
+ function localizeWorkbench(root=document.querySelector('.review-app')){if(!root)return;const elements=root.nodeType===1?[root,...root.querySelectorAll('*')]:[];for(const element of elements){if(i18nExcluded(element))continue;for(const node of element.childNodes)if(node.nodeType===Node.TEXT_NODE)localizeTextNode(node);for(const name of ['aria-label','title','placeholder'])localizeAttribute(element,name)}syncWorkbenchLocaleControls()}
1154
+ function syncWorkbenchLocaleControls(){document.querySelectorAll('[data-workbench-locale]').forEach(button=>button.setAttribute('aria-checked',String(button.dataset.workbenchLocale===workbenchLocale)));const current=$('.locale-current');if(current)current.textContent=workbenchLocale.toUpperCase()}
1155
+ function setWorkbenchLocale(locale,{remember=true,updateLocation=true}={}){const normalized=normalizeWorkbenchLocale(locale);if(!normalized)return;workbenchLocale=normalized;document.documentElement.lang=normalized;if(remember)try{localStorage.setItem(workbenchLocaleStorageKey,normalized)}catch(_){}if(updateLocation)writeLocation(false);renderView();renderCoverage();renderReviewHistory();renderAgentHandoff();localizeWorkbench();syncWorkbenchLocaleControls()}
1156
+ const workbenchLocaleObserver=new MutationObserver(records=>{for(const record of records){if(record.type==='characterData')localizeTextNode(record.target);else if(record.type==='attributes')localizeAttribute(record.target,record.attributeName);else for(const node of record.addedNodes){if(node.nodeType===Node.TEXT_NODE)localizeTextNode(node);else if(node.nodeType===Node.ELEMENT_NODE)localizeWorkbench(node)}}});
1157
+ function persist(){if(state.staleReview)return;localStorage.setItem(storageKey,JSON.stringify({workbenchSchema,diagnosticsEngineVersion,targetRevision:reviewTargetRevision,revision:reviewRevision,activeVersion:state.activeVersion,compareMode:state.compareMode,compareBaseVersion:state.compareBaseVersion,compareTargetVersion:state.compareTargetVersion,overlayPosition:state.overlayPosition,zoomMode:state.zoomMode,zoom:state.zoom,sidebarOpen:state.sidebarOpen,inspectorOpen:state.inspectorOpen,inspectorTab:state.inspectorTab,reviewSection:state.reviewSection,reviewScope:state.reviewScope,reviewSessionName:state.reviewSessionName,leftWidth:state.leftWidth,rightWidth:state.rightWidth,findingFilter:state.findingFilter,findingSource:state.findingSource,findingScreen:state.findingScreen,findingFocus:state.findingFocus,findingDecisions:state.findingDecisions,runtimeFindings:state.runtimeFindings,annotations:state.annotations,versionDecision:state.versionDecision,sourcePreparedVersionId:state.sourcePreparedVersionId,diagnostics:state.diagnostics,reviewRuns:state.reviewRuns,importedReview:state.importedReview,agentHandoff:state.agentHandoff,selectedNodeId:state.selectedNodeId,selectedScreenId:state.selectedScreenId,focusedFindingId:state.focusedFindingId,openFindingId:state.openFindingId,showFindings:state.showFindings,showBefore:state.showBefore,showAfter:state.showAfter,layerReturnView:state.layerReturnView,diagnosticTargetIds:state.diagnosticTargetIds,screenScenarioIds:state.screenScenarioIds,activeThemeId:state.activeThemeId,variantAxis:state.variantAxis,screenScrolls:state.screenScrolls,stageScroll:state.stageScroll}))}
1158
+ function mergeThemeTokens(base,patch){if(!patch||typeof patch!=='object'||Array.isArray(patch))return base;const result={...(base&&typeof base==='object'&&!Array.isArray(base)?base:{})};for(const [key,value] of Object.entries(patch))result[key]=value&&typeof value==='object'&&!Array.isArray(value)?mergeThemeTokens(result[key],value):value;return result}
1159
+ const themeTokenCache={};let currentRenderThemeId=defaultThemeId;function tokensForTheme(themeId){const resolved=themeById[themeId]?themeId:defaultThemeId;if(!themeTokenCache[resolved])themeTokenCache[resolved]=mergeThemeTokens(ir.tokens||{},themeById[resolved]?.tokenOverrides||{});return themeTokenCache[resolved]}
1160
+ function token(path,themeId=currentRenderThemeId){if(typeof path!=='string'||!path.startsWith('$'))return path;let value=tokensForTheme(themeId);for(const part of path.slice(1).split('.'))value=value?.[part];return value??path}
1161
+ function cssValue(value,unit='px'){value=token(value);if(typeof value==='number')return `${value}${unit}`;if(value==='fill')return '100%';if(value==='hug')return 'fit-content';return value??''}
1162
+ function styleFor(node){const l=node.layout||{},s=node.style||{},out={};const raw=(key,value)=>{if(value!=null)out[key]=token(value)};const length=(key,value)=>{if(value!=null)out[key]=cssValue(value)};const direction=l.direction||((['container','list','card'].includes(node.type))?'column':null);if(direction){out.display='flex';if(['row','column'].includes(direction))out.flexDirection=direction;if(direction==='grid'){out.display='grid';const columns=l.columns??2;out.gridTemplateColumns=typeof columns==='number'?`repeat(${columns},minmax(0,1fr))`:token(columns);if(l.rows!=null)out.gridTemplateRows=token(l.rows)}if(direction==='overlay'){out.position='relative';out.display='grid'}}length('gap',l.gap);length('rowGap',l.rowGap);length('columnGap',l.columnGap);length('padding',l.padding);length('margin',l.margin);if(l.paddingHorizontal!=null){length('paddingLeft',l.paddingHorizontal);length('paddingRight',l.paddingHorizontal)}if(l.paddingVertical!=null){length('paddingTop',l.paddingVertical);length('paddingBottom',l.paddingVertical)}if(l.marginHorizontal!=null){length('marginLeft',l.marginHorizontal);length('marginRight',l.marginHorizontal)}if(l.marginVertical!=null){length('marginTop',l.marginVertical);length('marginBottom',l.marginVertical)}for(const side of ['Top','Right','Bottom','Left']){length(`padding${side}`,l[`padding${side}`]);length(`margin${side}`,l[`margin${side}`])}for(const key of ['width','height','minWidth','maxWidth','minHeight','maxHeight','top','right','bottom','left','flexBasis'])length(key,l[key]);for(const key of ['position','display','overflow','overflowX','overflowY','order','zIndex','aspectRatio','gridArea'])raw(key,l[key]);raw('flexGrow',l.grow);raw('flexShrink',l.shrink);raw('flexWrap',l.wrap===true?'wrap':l.wrap);if(l.align)out.alignItems=({start:'flex-start',end:'flex-end',center:'center',stretch:'stretch',baseline:'baseline'}[l.align]||l.align);if(l.justify)out.justifyContent=({start:'flex-start',end:'flex-end',between:'space-between',around:'space-around',evenly:'space-evenly',center:'center'}[l.justify]||l.justify);if(l.alignSelf)out.alignSelf=({start:'flex-start',end:'flex-end',center:'center',stretch:'stretch',baseline:'baseline'}[l.alignSelf]||l.alignSelf);if(l.justifySelf)out.justifySelf=({start:'start',end:'end',center:'center',stretch:'stretch'}[l.justifySelf]||l.justifySelf);const lengths={borderWidth:'borderWidth',borderTopWidth:'borderTopWidth',borderRightWidth:'borderRightWidth',borderBottomWidth:'borderBottomWidth',borderLeftWidth:'borderLeftWidth',radius:'borderRadius',radiusTopLeft:'borderTopLeftRadius',radiusTopRight:'borderTopRightRadius',radiusBottomRight:'borderBottomRightRadius',radiusBottomLeft:'borderBottomLeftRadius',fontSize:'fontSize',lineHeight:'lineHeight',letterSpacing:'letterSpacing',outlineWidth:'outlineWidth'};const raws={background:'background',backgroundColor:'backgroundColor',backgroundImage:'backgroundImage',color:'color',border:'border',borderColor:'borderColor',borderTopColor:'borderTopColor',borderRightColor:'borderRightColor',borderBottomColor:'borderBottomColor',borderLeftColor:'borderLeftColor',borderStyle:'borderStyle',outlineColor:'outlineColor',outlineStyle:'outlineStyle',opacity:'opacity',fontFamily:'fontFamily',fontWeight:'fontWeight',fontStyle:'fontStyle',textAlign:'textAlign',textDecoration:'textDecoration',textTransform:'textTransform',whiteSpace:'whiteSpace',overflowWrap:'overflowWrap',textOverflow:'textOverflow',objectFit:'objectFit',objectPosition:'objectPosition',boxShadow:'boxShadow',filter:'filter',backdropFilter:'backdropFilter',transform:'transform',transformOrigin:'transformOrigin',cursor:'cursor',visibility:'visibility'};for(const [key,cssKey] of Object.entries(lengths))length(cssKey,s[key]);for(const [key,cssKey] of Object.entries(raws))raw(cssKey,s[key]);if(s.shadow!=null)raw('boxShadow',s.shadow);if(s.placeholderColor!=null)raw('--placeholder-color',s.placeholderColor);const typo=token(s.typography);if(typo&&typeof typo==='object'){for(const key of ['fontSize','lineHeight','letterSpacing'])length(key,typo[key]);for(const key of ['fontFamily','fontWeight','fontStyle','textTransform'])raw(key,typo[key])}if(s.css&&typeof s.css==='object')for(const [key,value] of Object.entries(s.css))if(/^(--[a-z0-9-]+|[a-zA-Z][a-zA-Z0-9-]*)$/.test(key))out[key]=token(value);return Object.entries(out).filter(([,v])=>v!==''&&v!=null).map(([k,v])=>`${k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase())}:${v}`).join(';')}
1163
+ function versionChain(versionId){const chain=[],seen=new Set();let current=versionById[versionId];while(current&&!seen.has(current.id)){seen.add(current.id);chain.unshift(current);current=current.parent?versionById[current.parent]:null}return chain}
1164
+ function versionOverride(nodeId,versionId){let result={};for(const version of versionChain(versionId)){const change=version.nodeOverrides?.[nodeId];if(change)result={...result,...change,layout:{...(result.layout||{}),...(change.layout||{})},style:{...(result.style||{}),...(change.style||{})}}}return result}
1165
+ function effectiveNode(id,versionId=state.activeVersion,nodeStateMap=state.nodeStates,nodeOverrides={}){const base=nodes[id];if(!base)return null;const themeChange=themeById[currentRenderThemeId]?.nodeOverrides?.[id]||{},change=versionOverride(id,versionId),scenarioChange=nodeOverrides?.[id]||{};let result={...base,...themeChange,...change,...scenarioChange,layout:{...(base.layout||{}),...(themeChange.layout||{}),...(change.layout||{}),...(scenarioChange.layout||{})},style:{...(base.style||{}),...(themeChange.style||{}),...(change.style||{}),...(scenarioChange.style||{})}};const variant=result.states?.[nodeStateMap?.[id]];if(variant)result={...result,...variant,layout:{...(result.layout||{}),...(variant.layout||{})},style:{...(result.style||{}),...(variant.style||{})}};return result}
1166
+ function changedInVersion(id,versionId){return versionChain(versionId).some(version=>version.kind!=='baseline'&&version.nodeOverrides?.[id])}
1167
+ function provenance(id,node,versionId){const src=node.source||{},action=node.action||{},semantics=node.semantics||{},interaction=nodeInteraction(node),standard=node.standardRef||Object.entries(node.standardRefs||{}).map(([platform,ref])=>`${platform}:${ref}`).join(', '),actionType=interaction.actionType,semanticRole=interaction.semanticRole,semanticAttrs=`${semanticRole?` role="${esc(semanticRole)}"`:''}${semantics.label&&interaction.interactive?` aria-label="${esc(semantics.label)}"`:''}`,actionAttrs=actionType?` data-action="${esc(actionType)}" data-target="${esc(action.target||'')}" data-action-state="${esc(action.state||'')}" data-action-off-state="${esc(action.offState||'')}"`:'';return `${semanticAttrs} data-interactive="${interaction.interactive}" data-interaction-contract="${interaction.version}" data-node-id="${esc(id)}" data-version-id="${esc(versionId)}" data-changed="${changedInVersion(id,versionId)}" data-source-file="${esc(src.file||'')}" data-source-line="${esc(src.line||'')}" data-source-symbol="${esc(src.symbol||'')}" data-component="${esc(node.component||'')}" data-component-ref="${esc(node.componentRef||'')}" data-confidence="${esc(node.confidence||'approximate')}" data-standard-ref="${esc(standard)}" data-decision-id="${esc(node.decisionId||'')}" data-semantic-role="${esc(semantics.role||'')}" data-semantic-label="${esc(semantics.label||'')}" data-target-size="${esc(typeof semantics.targetSize==='object'?`${semantics.targetSize.width||'?'}×${semantics.targetSize.height||'?'}`:(semantics.targetSize||''))}"${actionAttrs}`}
1168
+ function renderNode(id,stack=[],versionId=state.activeVersion,nodeStateMap=state.nodeStates,nodeOverrides={}){const n=effectiveNode(id,versionId,nodeStateMap,nodeOverrides);if(!n)return `<div class="ui-custom">Missing node ${esc(id)}</div>`;if(stack.includes(id))return `<div class="ui-custom">Circular node ${esc(id)}</div>`;const style=styleFor(n),p=provenance(id,n,versionId),children=(n.children||[]).map(child=>renderNode(child,[...stack,id],versionId,nodeStateMap,nodeOverrides)).join('');switch(n.type){case'text':return `<div class="ui-node ui-text" ${p} style="${esc(style)}">${esc(n.text||'')}</div>`;case'button':return `<button type="button" class="ui-node ui-button" ${p} ${n.disabled?'disabled':''} style="${esc(style)}">${children||esc(n.text||n.component||'')}</button>`;case'input':return `<input class="ui-node ui-input" ${p} type="${esc(n.inputType||'text')}" value="${esc(n.value||'')}" placeholder="${esc(n.placeholder||'')}" ${n.disabled?'disabled':''} style="${esc(style)}">`;case'image':return n.resolvedAsset?`<img class="ui-node ui-image" ${p} src="${n.resolvedAsset}" alt="${esc(n.alt||'')}" style="${esc(style)}">`:`<div class="ui-node ui-custom" ${p}>Asset unavailable: ${esc(n.asset||'')}</div>`;case'icon':return n.resolvedAsset?`<img class="ui-node ui-icon" ${p} src="${n.resolvedAsset}" alt="${esc(n.alt||'')}" style="${esc(style)}">`:`<span class="ui-node ui-icon" ${p} style="${esc(style)}">${esc(n.iconName||'◇')}</span>`;case'spacer':return `<div class="ui-node ui-spacer" ${p} style="${esc(style)}"></div>`;case'custom':return `<div class="ui-node ui-custom" ${p} style="${esc(style)}">${esc(n.text||n.component||'Unsupported UI')}</div>`;case'card':return `<div class="ui-node ui-card" ${p} style="${esc(style)}">${children}</div>`;case'list':return `<div class="ui-node ui-list" ${p} style="${esc(style)}">${children}</div>`;default:return `<div class="ui-node ui-container" ${p} style="${esc(style)}">${children}</div>`}}
1169
+ const vp=ir.viewport||{width:390,height:844,device:'phone'};function currentFromLocation(){const id=new URL(location.href).searchParams.get('screen');return screens.some(s=>s.id===id)?id:screens[0]?.id}function viewFromLocation(){const view=new URL(location.href).searchParams.get('view');return ['overview','prototype','single','states','compare'].includes(view)?view:'overview'}function themeFromLocation(){const id=new URL(location.href).searchParams.get('theme');return themeById[id]?id:defaultThemeId}function axisFromLocation(){const axis=new URL(location.href).searchParams.get('axis');return ['themes','states','matrix'].includes(axis)?axis:null}function viewportFor(screen){return {...vp,...(screen?.viewport||{})}}function themeDisplayLabel(theme){if(theme?.kind==='light'&&/^light$/i.test(theme.label||theme.id))return workbenchLocale==='ru'?'Светлая':'Light';if(theme?.kind==='dark'&&/^dark$/i.test(theme.label||theme.id))return workbenchLocale==='ru'?'Тёмная':'Dark';return theme?.label||theme?.id||'Theme'}
1170
+ function resolveScreenScenario(item){const fixture=item?.fixtureRef&&scenarioFixtures[item.fixtureRef]&&typeof scenarioFixtures[item.fixtureRef]==='object'?scenarioFixtures[item.fixtureRef]:{};return {...fixture,...item,nodeStates:{...(fixture.nodeStates||{}),...(item.nodeStates||{})},nodeOverrides:{...(fixture.nodeOverrides||{}),...(item.nodeOverrides||{})}}}
1171
+ function screenScenarios(screen){const declared=Array.isArray(screen?.scenarios)?screen.scenarios.filter(item=>item&&item.id).map(resolveScreenScenario):[];return [{id:'default',label:screen?.defaultScenarioLabel||'Данные',nodeStates:{},nodeOverrides:{}},...declared].slice(0,12)}
1172
+ function selectedScenario(screen){const scenarios=screenScenarios(screen),requested=state.screenScenarioIds[screen?.id]||new URL(location.href).searchParams.get('scenario')||screen?.defaultScenarioId||'default';return scenarios.find(item=>item.id===requested)||scenarios[0]}
1173
+ function scenarioNodeStates(scenario){return {...(scenario?.nodeStates||{}),...state.nodeStates}}
1174
+ function writeLocation(push=true){const u=new URL(location.href);u.searchParams.set('view',state.view);u.searchParams.set('lang',workbenchLocale);if(state.screen)u.searchParams.set('screen',state.screen);if(state.activeThemeId!==defaultThemeId)u.searchParams.set('theme',state.activeThemeId);else u.searchParams.delete('theme');if(state.view==='states')u.searchParams.set('axis',state.variantAxis);else u.searchParams.delete('axis');const screen=screens.find(item=>item.id===state.screen),scenario=screen?selectedScenario(screen):null;if(scenario&&scenario.id!=='default')u.searchParams.set('scenario',scenario.id);else u.searchParams.delete('scenario');const payload={view:state.view,screen:state.screen,scenario:scenario?.id||'default',theme:state.activeThemeId,axis:state.variantAxis,lang:workbenchLocale};push?history.pushState(payload,'',u):history.replaceState(payload,'',u)}
1175
+ function deviceMarkup(screen,extraClass='',versionId=state.activeVersion,scenario=selectedScenario(screen),themeId=state.activeThemeId){const screenVp=viewportFor(screen),nodeStateMap=scenarioNodeStates(scenario),resolvedThemeId=themeById[themeId]?themeId:defaultThemeId,previousThemeId=currentRenderThemeId;currentRenderThemeId=resolvedThemeId;const content=renderNode(screen.root,[],versionId,nodeStateMap,scenario?.nodeOverrides||{});currentRenderThemeId=previousThemeId;return `<div class="device ${esc(extraClass)}" data-screen-id="${esc(screen.id)}" data-scenario-id="${esc(scenario?.id||'default')}" data-theme-id="${esc(resolvedThemeId)}" data-version-id="${esc(versionId)}" data-width="${esc(screenVp.width)}" data-height="${esc(screenVp.height)}" data-device="${esc(screenVp.device||'phone')}" data-frame="${screenVp.frame===true}" data-background="${esc(screenVp.background||'')}"><div class="device-content">${content}</div></div>`}
1176
+ function configureDevice(device){device.style.width=`${device.dataset.width}px`;device.style.height=`${device.dataset.height}px`;device.classList.add(device.dataset.device==='desktop'?'desktop':'phone');if(device.dataset.frame==='true')device.classList.add('framed');device.querySelector('.device-content').style.background=token(device.dataset.background,device.dataset.themeId)||'transparent'}
1177
+ function overviewMarkup(){const stage=$('.stage'),gap=22,availableWidth=Math.max(320,(stage?.clientWidth||1200)-56),availableHeight=Math.max(320,(stage?.clientHeight||800)-56),cards=screens.map(screen=>{const screenVp=viewportFor(screen),scale=screenVp.device==='desktop'?0.2:0.52;return {screen,screenVp,scale,width:screenVp.width*scale,height:screenVp.height*scale+38}}),maxWidth=Math.max(240,...cards.map(item=>item.width)),maxHeight=Math.max(180,...cards.map(item=>item.height));let columns=1,bestFit=0;for(let candidate=1;candidate<=Math.min(8,Math.max(1,cards.length));candidate++){const rows=Math.ceil(cards.length/candidate),width=candidate*maxWidth+(candidate-1)*gap,height=rows*maxHeight+(rows-1)*gap,fit=Math.min(1,availableWidth/width,availableHeight/height);if(fit>bestFit+.001){columns=candidate;bestFit=fit}}const rows=Math.ceil(cards.length/columns),canvasWidth=columns*maxWidth+(columns-1)*gap,canvasHeight=rows*maxHeight+(rows-1)*gap;return `<div class="overview-canvas-shell"><div class="overview-canvas" style="width:${canvasWidth}px;min-height:${canvasHeight}px"><div class="overview-grid" style="display:grid;grid-template-columns:repeat(${columns},${maxWidth}px);gap:${gap}px">${cards.map(({screen,screenVp,scale,width})=>`<article class="screen-card" data-screen-card="${esc(screen.id)}" style="width:${width}px"><header class="screen-card-header"><span class="screen-card-title">${esc(screen.name)}</span><button type="button" class="screen-card-open" data-open-screen="${esc(screen.id)}">Открыть</button></header><div class="overview-viewport" style="width:${width}px;height:${screenVp.height*scale}px">${deviceMarkup(screen,'overview-device',state.activeVersion).replace('class="device overview-device"',`class="device overview-device" style="transform:scale(${scale})"`)}</div></article>`).join('')}</div></div></div>`}
1178
+ function compareMarkup(screen){const screenVp=viewportFor(screen),beforeId=versionById[state.compareBaseVersion]?state.compareBaseVersion:baselineVersion,afterId=versionById[state.compareTargetVersion]?state.compareTargetVersion:state.activeVersion,before=versionById[beforeId]||versions[0],after=versionById[afterId]||versions.at(-1);if(state.compareMode==='overlay')return `<div class="compare-overlay-wrap"><div class="compare-label">${esc(versionDisplayLabel(before))} ↔ ${esc(versionDisplayLabel(after))}</div><div class="compare-overlay" style="width:${screenVp.width}px;height:${screenVp.height}px;--overlay-width:${state.overlayPosition}%"><div class="before-layer">${deviceMarkup(screen,'',beforeId)}</div><div class="after-layer">${deviceMarkup(screen,'',afterId)}</div></div><input class="compare-overlay-slider" type="range" min="0" max="100" value="${state.overlayPosition}" aria-label="Наложение исходного и нового макета"></div>`;return `<div class="compare-grid"><article class="compare-panel"><div class="compare-label">${esc(versionDisplayLabel(before))}</div>${deviceMarkup(screen,'',beforeId)}</article><article class="compare-panel"><div class="compare-label">${esc(versionDisplayLabel(after))}</div>${deviceMarkup(screen,'',afterId)}</article></div>`}
1179
+ function variantCard(screen,scenario,theme){const current=scenario.id===selectedScenario(screen).id&&theme.id===state.activeThemeId;return `<article class="state-panel variant-card" data-state-panel="${esc(scenario.id)}" data-theme-panel="${esc(theme.id)}" data-current-variant="${current}" ${current?'aria-current="true"':''}><header><span>${esc(scenario.label||scenario.id)}</span><small>${esc(themeDisplayLabel(theme))}${scenario.description?` · ${esc(scenario.description)}`:''}</small></header>${deviceMarkup(screen,'',state.activeVersion,scenario,theme.id)}</article>`}
1180
+ function stateGalleryMarkup(screen){const scenarios=screenScenarios(screen),hasThemes=themes.length>1,hasStates=scenarios.length>1;let axis=state.variantAxis;if(axis==='matrix'&&(!hasThemes||!hasStates))axis=hasThemes?'themes':'states';if(axis==='themes'&&!hasThemes)axis='states';if(axis==='states'&&!hasStates)axis='themes';state.variantAxis=axis;const axisButtons=[['themes','По темам',hasThemes],['states','По состояниям',hasStates],['matrix','Матрица',hasThemes&&hasStates]].filter(item=>item[2]).map(([id,label])=>`<button type="button" data-variant-axis="${id}" aria-pressed="${axis===id}">${label}</button>`).join(''),axisSwitch=hasThemes&&hasStates?`<div class="variant-axis-switch" role="group" aria-label="${translateWorkbenchSource('Варианты экрана')}">${axisButtons}</div>`:'',selected=selectedScenario(screen),themeSelect=hasThemes?`<label class="screen-control-group"><span class="screen-state-label">${translateWorkbenchSource('Тема')}</span><select class="screen-state-select" data-gallery-theme aria-label="${translateWorkbenchSource('Тема')}">${themes.map(theme=>`<option value="${esc(theme.id)}" ${state.activeThemeId===theme.id?'selected':''}>${esc(themeDisplayLabel(theme))}</option>`).join('')}</select></label>`:'',scenarioSelect=hasStates?`<label class="screen-control-group scenario"><span class="screen-state-label">${translateWorkbenchSource('Состояние')}</span><select class="screen-state-select" data-gallery-scenario aria-label="${translateWorkbenchSource('Вариант данных экрана')}">${scenarios.map(scenario=>`<option value="${esc(scenario.id)}" ${selected.id===scenario.id?'selected':''}>${esc(scenario.label||scenario.id)}</option>`).join('')}</select></label>`:'';let sections;if(axis==='themes')sections=`<section class="variant-section"><header><b>${esc(selected.label||selected.id)}</b><span>${themes.length} · ${translateWorkbenchSource('Темы').toLocaleLowerCase()}</span></header><div class="state-gallery">${themes.map(theme=>variantCard(screen,selected,theme)).join('')}</div></section>`;else if(axis==='states')sections=`<section class="variant-section"><header><b>${esc(themeDisplayLabel(themeById[state.activeThemeId]))}</b><span>${scenarios.length} · ${translateWorkbenchSource('Состояния').toLocaleLowerCase()}</span></header><div class="state-gallery">${scenarios.map(scenario=>variantCard(screen,scenario,themeById[state.activeThemeId])).join('')}</div></section>`;else sections=themes.map(theme=>`<section class="variant-section"><header><b>${esc(themeDisplayLabel(theme))}</b><span>${scenarios.length} · ${translateWorkbenchSource('Состояния').toLocaleLowerCase()}</span></header><div class="state-gallery">${scenarios.map(scenario=>variantCard(screen,scenario,theme)).join('')}</div></section>`).join('');return `<div class="variant-gallery"><header class="variant-gallery-toolbar"><div class="variant-gallery-title"><b>${translateWorkbenchSource('Варианты экрана')}</b><span>${esc(screen.name)}</span></div><div class="variant-gallery-actions"><div class="variant-dimension-controls">${themeSelect}${scenarioSelect}</div>${axisSwitch}</div></header><div class="variant-canvas"><div class="variant-sections">${sections}</div></div></div>`}
1181
+ function renderScreenScenarioControls(screen){const controls=$('.screen-state-controls');if(!controls)return;const scenarios=screen?screenScenarios(screen):[],visible=Boolean(screen&&(scenarios.length>1||themes.length>1)&&['single','prototype'].includes(state.view));controls.hidden=!visible;if(!visible){controls.innerHTML='';return}const themeGroup=themes.length>1?`<label class="screen-control-group"><span class="screen-state-label">${translateWorkbenchSource('Тема')}</span><select class="screen-state-select" data-screen-theme aria-label="${translateWorkbenchSource('Тема')}">${themes.map(theme=>`<option value="${esc(theme.id)}" ${state.activeThemeId===theme.id?'selected':''}>${esc(themeDisplayLabel(theme))}</option>`).join('')}</select></label>`:'';const activeScenario=selectedScenario(screen),scenarioGroup=scenarios.length>1?`<label class="screen-control-group scenario"><span class="screen-state-label">${translateWorkbenchSource('Состояние')}</span><select class="screen-state-select" data-screen-scenario aria-label="${translateWorkbenchSource('Вариант данных экрана')}">${scenarios.map(scenario=>`<option value="${esc(scenario.id)}" ${activeScenario.id===scenario.id?'selected':''}>${esc(scenario.label||scenario.id)}</option>`).join('')}</select></label>`:'';controls.innerHTML=`${themeGroup}${scenarioGroup}<button type="button" class="open-variants" title="${translateWorkbenchSource('Разложить на холсте')}" aria-label="${translateWorkbenchSource('Разложить на холсте')}">${iconMarkup('grid')}<span>${translateWorkbenchSource('Варианты')}</span></button>`;controls.querySelector('[data-screen-scenario]')?.addEventListener('change',event=>{setScreenScenario(screen.id,event.currentTarget.value);requestAnimationFrame(()=>$('.screen-state-select[data-screen-scenario]')?.focus())});controls.querySelector('[data-screen-theme]')?.addEventListener('change',event=>{setTheme(event.currentTarget.value);requestAnimationFrame(()=>$('.screen-state-select[data-screen-theme]')?.focus())});controls.querySelector('.open-variants')?.addEventListener('click',()=>setView('states'))}
1182
+ function setScreenScenario(screenId,scenarioId){const screen=screens.find(item=>item.id===screenId);if(!screen||!screenScenarios(screen).some(item=>item.id===scenarioId))return;state.screenScenarioIds[screenId]=scenarioId;state.nodeStates={};persist();writeLocation(false);renderView()}
1183
+ function setTheme(themeId){if(!themeById[themeId])return;state.activeThemeId=themeId;state.nodeStates={};persist();writeLocation(false);renderView()}
1184
+ function setVariantAxis(axis){if(!['themes','states','matrix'].includes(axis))return;state.variantAxis=axis;persist();writeLocation(false);renderView()}
1185
+ const treePaths={};
1186
+ function inferredScreenTree(){const groups=new Map();for(const screen of screens){const routePart=String(screen.route||'').split(/[\/#]/).filter(Boolean)[0],key=screen.group||routePart||String(screen.id).split('-')[0]||'screens';if(!groups.has(key))groups.set(key,{id:`group-${key}`,label:key.charAt(0).toUpperCase()+key.slice(1),children:[]});groups.get(key).children.push({screenId:screen.id})}return [...groups.values()]}
1187
+ function treeCount(items){return (items||[]).reduce((sum,item)=>sum+(item.screenId||item.screen?1:treeCount(item.children)),0)}
1188
+ function treeMarkup(items,path=[],depth=0){return (items||[]).map(item=>{const screenId=item.screenId||item.screen;if(screenId){const screen=screens.find(value=>value.id===screenId);if(!screen)return '';const nextPath=[...path,item.label||screen.name];treePaths[screenId]=nextPath;return `<button type="button" class="screen-link" data-screen="${esc(screenId)}" aria-current="false" style="padding-left:${7+depth*6}px"><span>${esc(item.label||screen.name)}</span></button>`}const label=item.label||item.name||item.id||'Group',nextPath=[...path,label],children=item.children||[];return `<details class="screen-tree-group" data-tree-id="${esc(item.id||label)}" open><summary>${esc(label)}<span class="tree-count">${treeCount(children)}</span></summary><div class="screen-tree-children">${treeMarkup(children,nextPath,depth+1)}</div></details>`}).join('')}
1189
+ function renderScreenTree(){for(const key of Object.keys(treePaths))delete treePaths[key];$('.screen-list').innerHTML=treeMarkup(Array.isArray(ir.screenTree)&&ir.screenTree.length?ir.screenTree:inferredScreenTree());document.querySelectorAll('.screen-link').forEach(button=>{button.addEventListener('click',()=>setScreen(button.dataset.screen,['prototype','single','states'].includes(state.view)?state.view:'single'));button.addEventListener('mouseenter',()=>previewNavigationTarget(button.dataset.screen,true));button.addEventListener('mouseleave',()=>previewNavigationTarget(button.dataset.screen,false));button.addEventListener('focus',()=>previewNavigationTarget(button.dataset.screen,true));button.addEventListener('blur',()=>previewNavigationTarget(button.dataset.screen,false))})}
1190
+ function filterScreenTree(query){const value=String(query||'').trim().toLocaleLowerCase();document.querySelectorAll('.screen-link').forEach(button=>{button.hidden=Boolean(value)&&!button.textContent.toLocaleLowerCase().includes(value)});const groups=[...document.querySelectorAll('.screen-tree-group')].reverse();for(const group of groups){const visible=Boolean(group.querySelector('.screen-link:not([hidden]),.screen-tree-group:not([hidden])'));group.hidden=Boolean(value)&&!visible;if(value&&visible)group.open=true}}
1191
+ function updateTreeSelection(screenId){document.querySelectorAll('.screen-link').forEach(button=>button.setAttribute('aria-current',String(button.dataset.screen===screenId)));const active=document.querySelector(`.screen-link[data-screen="${CSS.escape(screenId||'')}"]`);let parent=active?.parentElement;while(parent){if(parent.matches?.('details'))parent.open=true;parent=parent.parentElement}}
1192
+ function renderInspectorTabs(){document.querySelectorAll('[data-inspector-tab]').forEach(button=>button.setAttribute('aria-pressed',String(state.inspectorOpen&&button.dataset.inspectorTab===state.inspectorTab)));document.querySelectorAll('[data-inspector-pane]').forEach(pane=>pane.hidden=pane.dataset.inspectorPane!==state.inspectorTab);const labels={inspect:'Свойства',review:'Ревью',comments:'Комментарии'},icons={inspect:'inspect',review:'review',comments:'comment'},title=$('.inspector-context-title'),icon=$('.inspector-context-icon'),count=$('.inspector-context-count');if(title)title.textContent=labels[state.inspectorTab];if(icon)icon.innerHTML=iconMarkup(icons[state.inspectorTab]);if(count){const value=state.inspectorTab==='review'?findings.length:state.inspectorTab==='comments'?state.annotations.length:0;count.textContent=value?String(value):'';count.hidden=!value}}
1193
+ function setInspectorTab(tab,open=true){if(!['inspect','review','comments'].includes(tab))return;state.inspectorTab=tab;if(open){state.inspectorOpen=true;if(innerWidth<=980)state.sidebarOpen=false}persist();applyPanelState();renderInspectorTabs()}
1194
+ function activateRailTool(tab){if(!['inspect','review','comments'].includes(tab))return;state.interaction=tab==='inspect'?'inspect':tab==='comments'?'comment':'interact';state.showFindings=tab==='review';setInspectorTab(tab);syncInteractionButtons();renderView()}
1195
+ function reviewScopeScreens(){if(state.reviewScope==='current'){const current=screens.find(item=>item.id===state.screen)||screens[0];return current?[current]:[]}return screens}
1196
+ function nodeIdsForScreen(screen){const result=[],seen=new Set(),walk=id=>{if(!id||seen.has(id)||!nodes[id])return;seen.add(id);result.push(id);for(const child of nodes[id].children||[])walk(child)};walk(screen?.root);return result}
1197
+ function stateVariantCases(targetScreens=reviewScopeScreens()){const result=[];for(const screen of targetScreens){for(const theme of themes.slice(1))result.push({kind:'theme',screenId:screen.id,themeId:theme.id,stateName:`theme:${theme.id}`});for(const scenario of screenScenarios(screen).slice(1))result.push({kind:'screen',screenId:screen.id,scenarioId:scenario.id,stateName:`scenario:${scenario.id}`});for(const nodeId of nodeIdsForScreen(screen)){const variants=nodes[nodeId]?.states;if(!variants||typeof variants!=='object')continue;for(const stateName of Object.keys(variants))result.push({kind:'component',screenId:screen.id,nodeId,stateName})}}return result}
1198
+ function renderReviewSections(){const allVerified=findings.length>0&&findings.every(findingResolved),selected=selectedFindings().length;document.querySelectorAll('[data-review-section]').forEach(button=>{const section=button.dataset.reviewSection;button.setAttribute('aria-pressed',String(section===state.reviewSection));button.classList.toggle('complete',section==='summary'?Boolean(state.diagnostics):section==='problems'?(!findings.length||selected>0||allVerified):allVerified)});document.querySelectorAll('[data-review-workspace]').forEach(section=>section.hidden=section.dataset.reviewWorkspace!==state.reviewSection);renderReviewNextAction()}
1199
+ function setReviewSection(section){if(!['summary','problems','changes'].includes(section))return;state.reviewSection=section;persist();renderReviewSections();document.querySelector('[data-inspector-pane="review"]')?.scrollTo({top:0,behavior:'smooth'})}
1200
+ function diagnosticProfileLabel(profile){return profile.viewport==='current'?profile.label:`${profile.viewport?.width||'?'}×${profile.viewport?.height||'?'}`}
1201
+ function isActionableDiagnostic(check){return check.result!=='pass'&&check.metrics?.scope!=='workbench'}
1202
+ function renderCoverage(){const body=$('.coverage-body'),status=$('.coverage-status');if(!body)return;const targetScreens=reviewScopeScreens(),profiles=diagnosticProfiles(),variants=stateVariantCases(targetScreens),checks=state.diagnostics?.checks||[],stateChecks=checks.filter(item=>item.scenarioId==='state-matrix'),flowChecks=checks.filter(item=>item.scenarioId==='navigation-flow'),stateKey=item=>item.metrics?.stateNodeId||item.metrics?.screenScenarioId||item.metrics?.themeId,testedStates=new Set(stateChecks.filter(item=>stateKey(item)).map(item=>`${item.screenId}:${stateKey(item)}:${item.metrics.stateName}:${item.metrics.profileId}`)),expectedStates=variants.length*profiles.length,missingStates=Math.max(0,expectedStates-testedStates.size),actionable=checks.filter(isActionableDiagnostic).length;status.textContent=state.diagnosticsRunning?'проверяется':state.diagnostics?'готово':'не запускалось';const screenRows=targetScreens.map(screen=>{const screenChecks=checks.filter(item=>item.screenId===screen.id),screenVariants=variants.filter(item=>item.screenId===screen.id).length*profiles.length,screenTested=new Set(stateChecks.filter(item=>item.screenId===screen.id&&stateKey(item)).map(item=>`${stateKey(item)}:${item.metrics.stateName}:${item.metrics.profileId}`)).size,screenProblems=screenChecks.filter(isActionableDiagnostic);return `<div class="coverage-row"><span>${esc(screen.name)}</span><span class="coverage-cell ${screenChecks.length?'pass':'gap'}">${screenChecks.length}</span><span class="coverage-cell ${screenVariants===screenTested?'pass':'gap'}">${screenTested}/${screenVariants}</span><span class="coverage-cell ${screenProblems.length?'gap':'pass'}">${screenProblems.length}</span></div>`}).join('');const gapText=!state.diagnostics?'Покрытие станет измеримым после запуска.':missingStates?`Не проверено вариантов состояния/тем: ${missingStates}. Полное покрытие не заявляется.`:`Проверены все объявленные состояния и темы в выбранной области. Переходов проверено: ${flowChecks.length}.`;body.innerHTML=`<div class="coverage-grid"><div class="coverage-metric"><b>${targetScreens.length}</b><span>экранов в области</span></div><div class="coverage-metric"><b>${profiles.length}</b><span>реальных viewport-профиля</span></div><div class="coverage-metric"><b>${checks.length}</b><span>выполнено проверок</span></div><div class="coverage-metric"><b>${actionable}</b><span>проблем продукта</span></div></div><div class="coverage-gaps ${state.diagnostics&&!missingStates?'pass':''}">${gapText}</div><div class="coverage-matrix"><div class="coverage-row header"><span>Экран</span><span>Тесты</span><span>Вар.</span><span>Пробл.</span></div>${screenRows}</div>`}
1203
+ function renderReviewHistory(){const list=$('.review-history-list'),name=$('.review-session-name');if(name)name.value=state.reviewSessionName;if(!list)return;const stored=state.reviewRuns.slice(-6).reverse(),runs=stored.length?stored:(state.diagnostics?[{sessionName:state.reviewSessionName,scopeLabel:state.diagnostics.scope==='current'?'Текущий экран':'Все экраны',issues:groupDiagnosticChecks((state.diagnostics.checks||[]).filter(item=>item.result!=='pass')).length,checks:state.diagnostics.checks?.length||0,stateCases:(state.diagnostics.checks||[]).filter(item=>item.scenarioId==='state-matrix').length,createdAt:state.diagnostics.completedAt}]:[]);list.innerHTML=runs.length?runs.map((run,index)=>{const previous=runs[index+1],delta=previous?run.issues-previous.issues:null;return `<div class="review-history-item"><span>${esc(run.sessionName||state.reviewSessionName||'Ревью')} · ${esc(run.scopeLabel||run.scope||'Все экраны')}</span><b>${run.issues} проблем${delta==null?'':` · ${delta>0?'+':''}${delta}`}</b><small>${new Date(run.createdAt).toLocaleString(workbenchLocale)} · ${run.checks} проверок · ${run.stateCases||0} состояний</small></div>`}).join(''):'<div class="panel-empty compact"><strong>Запусков пока нет</strong><span>История появится после первой проверки.</span></div>'}
1204
+ function applyPanelState(){const app=$('.review-app');app.classList.toggle('sidebar-hidden',!state.sidebarOpen);app.classList.toggle('inspector-hidden',!state.inspectorOpen);app.style.setProperty('--left-panel',state.sidebarOpen?`${state.leftWidth}px`:'0px');app.style.setProperty('--left-grip',state.sidebarOpen?'4px':'0px');app.style.setProperty('--right-panel',state.inspectorOpen?`${state.rightWidth}px`:'0px');app.style.setProperty('--right-grip',state.inspectorOpen?'4px':'0px');document.querySelectorAll('.sidebar-toggle').forEach(button=>{button.setAttribute('aria-pressed',String(state.sidebarOpen));button.title=state.sidebarOpen?'Скрыть панель экранов':'Показать панель экранов'});document.querySelectorAll('.inspector-toggle').forEach(button=>{button.setAttribute('aria-pressed',String(state.inspectorOpen));button.title=state.inspectorOpen?'Скрыть правую панель':'Показать правую панель'});renderInspectorTabs()}
1205
+ function bindResizer(handle){const side=handle.dataset.resize;const minimum=side==='left'?190:280,maximum=side==='left'?420:560;const update=value=>{if(side==='left')state.leftWidth=Math.max(minimum,Math.min(maximum,value));else state.rightWidth=Math.max(minimum,Math.min(maximum,value));applyPanelState()};handle.addEventListener('pointerdown',event=>{event.preventDefault();handle.setPointerCapture(event.pointerId);const startX=event.clientX,start=side==='left'?state.leftWidth:state.rightWidth;$('.review-app').classList.add('resizing');const move=moveEvent=>update(start+(side==='left'?moveEvent.clientX-startX:startX-moveEvent.clientX));const stop=()=>{handle.removeEventListener('pointermove',move);$('.review-app').classList.remove('resizing');persist();renderView()};handle.addEventListener('pointermove',move);handle.addEventListener('pointerup',stop,{once:true});handle.addEventListener('pointercancel',stop,{once:true})});handle.addEventListener('dblclick',()=>{update(side==='left'?244:324);persist();renderView()});handle.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight'].includes(event.key))return;event.preventDefault();const direction=event.key==='ArrowRight'?1:-1;update((side==='left'?state.leftWidth:state.rightWidth)+(side==='left'?direction:-direction)*10);persist();renderView()})}
1206
+ function clampZoom(value){return Math.max(.2,Math.min(2,Math.round(value*20)/20))}
1207
+ function applyZoom(screen){const stage=$('.stage'),label=$('.zoom-label');if(!stage||!screen)return;const screenVp=viewportFor(screen),overviewCanvas=stage.querySelector('.overview-canvas');let zoom=state.zoom;if(state.zoomMode==='fit'){if(state.view==='overview'&&overviewCanvas){zoom=clampZoom(Math.min(1,(stage.clientWidth-56)/overviewCanvas.scrollWidth,(stage.clientHeight-56)/overviewCanvas.scrollHeight))}else if(state.view!=='overview'){const visibleVariants=state.variantAxis==='themes'?themes.length:screenScenarios(screen).length,horizontalScreens=state.view==='states'?Math.min(2,Math.max(1,visibleVariants)):1;zoom=clampZoom(Math.min(1,(stage.clientWidth-72)/(screenVp.width*horizontalScreens+Math.max(0,horizontalScreens-1)*28),(stage.clientHeight-126)/screenVp.height))}}state.computedZoom=zoom;if(state.view==='overview'&&overviewCanvas){const shell=overviewCanvas.closest('.overview-canvas-shell');overviewCanvas.style.transform=`scale(${zoom})`;if(shell){shell.style.width=`${Math.ceil(overviewCanvas.scrollWidth*zoom)}px`;shell.style.height=`${Math.ceil(overviewCanvas.scrollHeight*zoom)}px`}}else if(state.view==='compare'){const target=state.compareMode==='split'?$('.compare-grid'):$('.compare-overlay-wrap');if(target)target.style.zoom=zoom}else if(state.view==='states'){const canvas=$('.variant-canvas');if(canvas)canvas.style.zoom=zoom}else{const device=stage.querySelector(':scope>.device');if(device)device.style.zoom=zoom}if(label)label.textContent=`${Math.round(zoom*100)}%`}
1208
+ function setZoom(value){state.zoomMode='manual';state.zoom=clampZoom(value);persist();const screen=screens.find(item=>item.id===state.screen)||screens[0];applyZoom(screen);scheduleFindingMarkers()}
1209
+ function handleZoomWheel(event){if(event.target.closest('.device-content')&&!event.ctrlKey&&!event.metaKey)return;event.preventDefault();setZoom(state.computedZoom+(event.deltaY<0?.1:-.1))}
1210
+ let canvasPan=null;function beginCanvasPan(event){if(event.button!==1)return;const stage=event.currentTarget;event.preventDefault();canvasPan={pointerId:event.pointerId,x:event.clientX,y:event.clientY,left:stage.scrollLeft,top:stage.scrollTop};stage.classList.add('canvas-panning');try{stage.setPointerCapture(event.pointerId)}catch(_){}}
1211
+ function moveCanvasPan(event){if(!canvasPan||event.pointerId!==canvasPan.pointerId)return;event.preventDefault();const stage=event.currentTarget;stage.scrollLeft=canvasPan.left-(event.clientX-canvasPan.x);stage.scrollTop=canvasPan.top-(event.clientY-canvasPan.y);scheduleFindingMarkers()}
1212
+ function endCanvasPan(event){if(!canvasPan||event.pointerId!==canvasPan.pointerId)return;event.preventDefault();const stage=event.currentTarget;try{stage.releasePointerCapture(event.pointerId)}catch(_){}stage.classList.remove('canvas-panning');canvasPan=null;scheduleFindingMarkers()}
1213
+ function bindCanvasPan(stage){if(!stage||stage.dataset.canvasPanBound==='true')return;stage.dataset.canvasPanBound='true';stage.addEventListener('pointerdown',beginCanvasPan);stage.addEventListener('pointermove',moveCanvasPan);stage.addEventListener('pointerup',endCanvasPan);stage.addEventListener('pointercancel',endCanvasPan);stage.addEventListener('lostpointercapture',event=>{if(canvasPan?.pointerId===event.pointerId){stage.classList.remove('canvas-panning');canvasPan=null;scheduleFindingMarkers()}});stage.addEventListener('auxclick',event=>{if(event.button===1)event.preventDefault()})}
1214
+ function captureViewContext(stage){if(!stage)return;state.stageScroll={left:stage.scrollLeft,top:stage.scrollTop};const previousScreen=stage.dataset.renderedScreen,content=stage.querySelector('.device-content');if(previousScreen&&content)state.screenScrolls[previousScreen]={left:content.scrollLeft,top:content.scrollTop};stage.onscroll=null;if(content)content.onscroll=null}
1215
+ function restoreViewContext(stage,screen){stage.dataset.renderedScreen=screen?.id||'';const selected=state.selectedScreenId===screen?.id&&state.selectedNodeId?stage.querySelector(`[data-node-id="${CSS.escape(state.selectedNodeId)}"]`):null;selectNode(selected,false);const focused=findings.find(item=>item.id===state.focusedFindingId);if(focused?.screenId===screen?.id&&focused.nodeId)stage.querySelectorAll(`[data-node-id="${CSS.escape(focused.nodeId)}"]`).forEach(node=>node.dataset.findingSeverity=focused.severity||'medium');for(const id of state.diagnosticTargetIds)stage.querySelectorAll(`[data-node-id="${CSS.escape(id)}"]`).forEach(node=>node.dataset.diagnosticTarget='true');const stageScroll={...state.stageScroll},savedScroll={...(state.screenScrolls[screen?.id]||{})};requestAnimationFrame(()=>{stage.scrollLeft=stageScroll.left||0;stage.scrollTop=stageScroll.top||0;const contents=[...stage.querySelectorAll('.device-content')];for(const content of contents){content.scrollLeft=savedScroll.left||0;content.scrollTop=savedScroll.top||0;content.onscroll=()=>{state.screenScrolls[screen.id]={left:content.scrollLeft,top:content.scrollTop};scheduleFindingMarkers()}}stage.onscroll=()=>{state.stageScroll={left:stage.scrollLeft,top:stage.scrollTop};scheduleFindingMarkers()};scheduleFindingMarkers()})}
1216
+ function renderView(){const screen=screens.find(item=>item.id===state.screen)||screens[0],hasVariants=Boolean(screen&&(screenScenarios(screen).length>1||themes.length>1));state.screen=screen?.id;if(state.view==='compare'){const afterId=activeAfterVersion();if(afterId){state.showBefore=true;state.showAfter=true;state.compareBaseVersion=baselineVersion;state.compareTargetVersion=afterId;state.activeVersion=afterId}else state.view='single'}if(state.view==='states'&&!hasVariants)state.view='single';if(state.view==='compare')renderCompareVersionOptions();const app=$('.review-app'),stage=$('.stage');captureViewContext(stage);app.dataset.view=state.view;app.classList.toggle('has-screen-states',hasVariants);document.querySelectorAll('[data-view]').forEach(button=>{button.setAttribute('aria-pressed',String(button.dataset.view===state.view));if(button.dataset.view==='states')button.disabled=!hasVariants});document.querySelectorAll('[data-compare-mode]').forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.compareMode===state.compareMode)));updateTreeSelection(state.screen);$('.screen-title').textContent=state.view==='overview'?`Все экраны · ${screens.length}`:(treePaths[state.screen]||[screen?.name||'']).join(' / ');stage.className=`stage ${state.view}${state.interaction==='inspect'?' inspect-mode':''}${state.interaction==='comment'?' comment-mode':''}`;stage.innerHTML=state.view==='overview'?overviewMarkup():(state.view==='compare'&&screen?compareMarkup(screen):(state.view==='states'&&screen?stateGalleryMarkup(screen):(screen?deviceMarkup(screen):'<div class="empty">Нет экранов</div>')));document.querySelectorAll('.device').forEach(configureDevice);renderScreenScenarioControls(screen);applyZoom(screen);stage.onwheel=handleZoomWheel;bindNodes();document.querySelectorAll('[data-open-screen]').forEach(button=>button.addEventListener('click',()=>setScreen(button.dataset.openScreen,'single')));document.querySelectorAll('[data-variant-axis]').forEach(button=>button.addEventListener('click',()=>setVariantAxis(button.dataset.variantAxis)));$('[data-gallery-scenario]')?.addEventListener('change',event=>{setScreenScenario(screen.id,event.currentTarget.value);requestAnimationFrame(()=>$('.screen-state-select[data-gallery-scenario]')?.focus())});$('[data-gallery-theme]')?.addEventListener('change',event=>{setTheme(event.currentTarget.value);requestAnimationFrame(()=>$('.screen-state-select[data-gallery-theme]')?.focus())});const slider=$('.compare-overlay-slider');if(slider)slider.addEventListener('input',()=>{state.overlayPosition=Number(slider.value);$('.compare-overlay').style.setProperty('--overlay-width',`${state.overlayPosition}%`);persist()});restoreViewContext(stage,screen);renderFindings();renderQueue();renderDiagnostics();renderRevisionNotice();applyPanelState();syncCanvasLayerControls();requestAnimationFrame(renderFindingMarkers)}
1217
+ function setView(view,push=true){if(!['overview','prototype','single','states','compare'].includes(view))return;const screen=screens.find(item=>item.id===state.screen)||screens[0],hasVariants=Boolean(screen&&(screenScenarios(screen).length>1||themes.length>1));if(view==='states'&&!hasVariants){showToast('Для этого экрана доступен только один вариант');return}if(view==='prototype'){state.interaction='interact';syncInteractionButtons()}if(view==='compare'){const afterId=activeAfterVersion();if(!afterId){showToast('Для сравнения пока нет версии «После»');return}if(state.view!=='compare')state.layerReturnView=state.view;state.showBefore=true;state.showAfter=true;state.compareBaseVersion=baselineVersion;state.compareTargetVersion=afterId;state.activeVersion=afterId}else if(state.showBefore&&state.showAfter){state.showBefore=state.activeVersion===baselineVersion;state.showAfter=!state.showBefore}state.view=view;persist();writeLocation(push);renderView()}function setScreen(id,view='single',push=true){if(!screens.some(screen=>screen.id===id))return;state.screen=id;state.nodeStates={};const screen=screens.find(item=>item.id===id);if(view==='states'&&screenScenarios(screen).length<2&&themes.length<2)view='single';if(view==='compare'){const afterId=activeAfterVersion();if(afterId){state.showBefore=true;state.showAfter=true;state.compareBaseVersion=baselineVersion;state.compareTargetVersion=afterId;state.activeVersion=afterId}else view='single'}state.view=view;writeLocation(push);renderView()}function navigate(id,push=true){setScreen(id,'prototype',push)}
1218
+ let toastTimer;function showToast(message){const toast=$('.workbench-toast');toast.textContent=message;toast.classList.add('visible');clearTimeout(toastTimer);toastTimer=setTimeout(()=>toast.classList.remove('visible'),2600)}
1219
+ function closeWorkbenchMenus(except=null){document.querySelectorAll('.menu[open]').forEach(menu=>{if(menu!==except)menu.removeAttribute('open')})}
1220
+ function bindWorkbenchMenus(){document.querySelectorAll('.menu').forEach(menu=>{menu.addEventListener('toggle',()=>{if(menu.open)closeWorkbenchMenus(menu)});menu.querySelector('.menu-popover')?.addEventListener('click',event=>{if(event.target.closest?.('button'))closeWorkbenchMenus()})});document.addEventListener('pointerdown',event=>{if(!event.target.closest?.('.menu'))closeWorkbenchMenus()});document.addEventListener('keydown',event=>{if(event.key!=='Escape')return;const openMenu=document.querySelector('.menu[open]');if(!openMenu)return;closeWorkbenchMenus();openMenu.querySelector('summary')?.focus()})}
1221
+ function applyAction(el,event){const type=el.dataset.action,target=el.dataset.target;if(!type||state.view==='overview'||state.view==='compare')return;if(type==='navigate'){event.preventDefault();event.stopPropagation();if(state.view==='prototype')navigate(target);else showToast('Переходы выключены. Выберите режим «Прототип».');return}if(type==='back'){event.preventDefault();event.stopPropagation();history.back();return}if(type==='set-node-state'){event.preventDefault();event.stopPropagation();state.nodeStates[target]=el.dataset.actionState;renderView();return}if(type==='toggle-node-state'||type==='toggle'){event.preventDefault();event.stopPropagation();state.nodeStates[target]=state.nodeStates[target]===el.dataset.actionState?(el.dataset.actionOffState||'default'):el.dataset.actionState;renderView();return}if(type==='reset-state'){event.preventDefault();event.stopPropagation();state.nodeStates={};renderView()}}
1222
+ function previewNavigationTarget(screenId,visible){document.querySelectorAll('.screen-link[data-navigation-preview=true]').forEach(item=>item.removeAttribute('data-navigation-preview'));const label=$('.navigation-preview-label');if(!visible||!screens.some(screen=>screen.id===screenId)){label.classList.remove('visible');label.textContent='';return}const target=document.querySelector(`.screen-link[data-screen="${CSS.escape(screenId)}"]`);if(target){target.dataset.navigationPreview='true';let parent=target.parentElement;while(parent){if(parent.matches?.('details'))parent.open=true;parent=parent.parentElement}}label.textContent=`Откроется: ${(treePaths[screenId]||[screenId]).join(' / ')}`;label.classList.add('visible')}
1223
+ function bindNodes(){document.querySelectorAll('.ui-node').forEach(el=>{el.addEventListener('click',event=>{if(['inspect','comment'].includes(state.interaction)){event.preventDefault();event.stopPropagation();selectNode(el);return}applyAction(el,event)});if(el.dataset.action==='navigate'){el.addEventListener('mouseenter',()=>previewNavigationTarget(el.dataset.target,true));el.addEventListener('mouseleave',()=>previewNavigationTarget(el.dataset.target,false));el.addEventListener('focus',()=>previewNavigationTarget(el.dataset.target,true));el.addEventListener('blur',()=>previewNavigationTarget(el.dataset.target,false))}})}
1224
+ function inspectorMarkup(el){if(!el)return `<div class="panel-empty">${iconMarkup('inspect')}<strong>Выберите элемент</strong><span>Свойства, источник и стандарт появятся здесь.</span></div>`;const c=el.dataset.confidence||'approximate',node=nodes[el.dataset.nodeId]||{},entries=Object.entries(node.provenance||{}),propertyRows=entries.length?`<details class="warning-panel" open><summary>Происхождение свойств · ${entries.length}</summary><dl class="inspect-table">${entries.map(([path,item])=>`<dt>${esc(path)}</dt><dd>${esc(item.file||'—')}${item.line?':'+esc(item.line):''}<br><span class="confidence ${esc(item.confidence||'approximate')}">${esc(item.adapter||'source')} · ${esc(item.confidence||'approximate')}</span></dd>`).join('')}</dl></details>`:'';return `<dl class="inspect-table"><dt>Узел</dt><dd>${esc(el.dataset.nodeId)}</dd><dt>Версия</dt><dd>${esc(el.dataset.versionId)}</dd><dt>Компонент</dt><dd>${esc(el.dataset.component||'—')}</dd><dt>Каталог</dt><dd>${esc(el.dataset.componentRef||'—')}</dd><dt>Источник</dt><dd>${esc(el.dataset.sourceFile||'—')}${el.dataset.sourceLine?':'+esc(el.dataset.sourceLine):''}</dd><dt>Символ</dt><dd>${esc(el.dataset.sourceSymbol||'—')}</dd><dt>Стандарт</dt><dd>${esc(el.dataset.standardRef||'—')}</dd><dt>Решение</dt><dd>${esc(el.dataset.decisionId||'—')}</dd><dt>Семантика</dt><dd>${esc(el.dataset.semanticRole||'—')}${el.dataset.semanticLabel?' · '+esc(el.dataset.semanticLabel):''}</dd><dt>Цель</dt><dd>${esc(el.dataset.targetSize||'—')}</dd><dt>Точность</dt><dd><span class="confidence ${esc(c)}">${esc(c)}</span></dd></dl>${propertyRows}`}
1225
+ function commentComposerMarkup(el){if(!el)return `<div class="panel-empty compact">${iconMarkup('comment')}<strong>Выберите элемент</strong><span>Комментарий будет привязан к выбранному узлу.</span></div>`;return `<form class="comment-form"><div class="comment-target">${esc(treePaths[state.screen]?.join(' / ')||state.screen)} · ${esc(el.dataset.nodeId)}</div><textarea name="comment" required placeholder="Что нужно изменить?"></textarea><div class="comment-form-row"><select name="priority"><option value="medium">Средний приоритет</option><option value="high">Высокий приоритет</option><option value="low">Низкий приоритет</option></select><button type="submit">Добавить</button></div></form>`}
1226
+ function bindCommentForm(form,el){if(!form||!el)return;form.addEventListener('submit',event=>{event.preventDefault();const data=new FormData(form),text=String(data.get('comment')||'').trim();if(!text)return;const id=`annotation-${Date.now()}-${Math.random().toString(36).slice(2,7)}`;state.annotations.push({id,screenId:state.screen,nodeId:el.dataset.nodeId,versionId:el.dataset.versionId||state.activeVersion,text,priority:data.get('priority')||'medium',status:'new',source:{file:el.dataset.sourceFile||'',line:Number(el.dataset.sourceLine)||null,symbol:el.dataset.sourceSymbol||''},createdAt:new Date().toISOString()});persist();form.reset();renderQueue();scheduleFindingMarkers();showToast('Комментарий добавлен')})}
1227
+ function selectNode(el,remember=true){document.querySelectorAll('[data-selected=true]').forEach(n=>n.removeAttribute('data-selected'));document.querySelectorAll('[data-finding-severity]').forEach(n=>n.removeAttribute('data-finding-severity'));state.selected=el;if(remember){state.selectedNodeId=el?.dataset.nodeId||null;state.selectedScreenId=el?state.screen:null;if(state.interaction==='inspect')setInspectorTab('inspect');if(state.interaction==='comment')setInspectorTab('comments');persist()}if(el)el.dataset.selected='true';$('.inspect-body').innerHTML=inspectorMarkup(el);const composer=$('.comment-composer');if(composer){composer.innerHTML=commentComposerMarkup(el);bindCommentForm(composer.querySelector('.comment-form'),el)}}
1228
+ const findingRank={blocker:4,high:3,medium:2,low:1};
1229
+ function addFindingBadges(){document.querySelectorAll('.screen-issue-count').forEach(item=>item.remove());const byScreen=new Map();for(const item of findings){const current=byScreen.get(item.screenId)||{count:0,severity:'low'};current.count+=1;if((findingRank[item.severity]||0)>(findingRank[current.severity]||0))current.severity=item.severity;byScreen.set(item.screenId,current)}for(const [screenId,data] of byScreen){const target=document.querySelector(`.screen-link[data-screen="${CSS.escape(screenId)}"]`);if(!target)continue;const badge=document.createElement('span');badge.className='screen-issue-count';badge.dataset.severity=data.severity;badge.textContent=String(data.count);badge.title=`Замечаний: ${data.count}`;target.append(badge)}}
1230
+ function orderedFindings(values=[...findings]){const sourceOrder=new Map([...findings].map((item,index)=>[item.id,index]));return [...values].sort((a,b)=>(findingRank[b.severity]||0)-(findingRank[a.severity]||0)||(sourceOrder.get(a.id)||0)-(sourceOrder.get(b.id)||0))}
1231
+ function findingNumber(id){return orderedFindings().findIndex(item=>item.id===id)+1}
1232
+ function resolvedFindingIdsForVersion(versionId){return new Set(versionChain(versionId).flatMap(version=>version.resolvedFindingIds||[]))}
1233
+ function findingReviewVersion(item){return item.reviewVersionId||item.versionId||baselineVersion}
1234
+ function findingResolved(item){const verification=item.verification||{};return verification.result==='pass'||verification.status==='verified'||Boolean(item.verifiedAt)}
1235
+ function findingAddressed(item,versionId=state.activeVersion){return resolvedFindingIdsForVersion(versionId).has(item.id)}
1236
+ function findingProposalId(item){if(!item)return null;if(item.proposalVersionId&&versionById[item.proposalVersionId])return item.proposalVersionId;return [...proposalVersions()].reverse().find(version=>resolvedFindingIdsForVersion(version.id).has(item.id))?.id||null}
1237
+ function findingAddressedInProposal(item){return Boolean(findingProposalId(item))}
1238
+ function findingAppliesToVersion(item,versionId){return versionId===findingReviewVersion(item)&&!findingResolved(item)}
1239
+ function visibleCanvasVersionIds(){if(state.view==='compare')return [state.compareBaseVersion,state.compareTargetVersion].filter(Boolean);return [state.activeVersion]}
1240
+ function findingsForCurrentSurface(){const versionIds=visibleCanvasVersionIds();return [...findings].filter(item=>versionIds.some(versionId=>findingAppliesToVersion(item,versionId)))}
1241
+ function findingsForCanvas(){let result=state.findingFocus.length?findingsForCurrentSurface().filter(item=>state.findingFocus.includes(item.id)):findingsForCurrentSurface();if(!state.findingFocus.length&&state.findingFilter==='accepted')result=result.filter(item=>state.findingDecisions[item.id]==='accepted');else if(!state.findingFocus.length&&!['all','linked'].includes(state.findingFilter))result=result.filter(item=>item.severity===state.findingFilter);result=result.filter(item=>{const origin=findingOrigin(item),sourceMatch=state.findingSource==='all'||state.findingSource===origin||(state.findingSource==='systemic'&&item.systemic),screenMatch=state.findingScreen==='all'||item.screenId===state.findingScreen;return sourceMatch&&screenMatch});return orderedFindings(result)}
1242
+ function activeAfterVersion(){const candidates=proposalVersions();if(!candidates.length)return null;if(versionById[state.compareTargetVersion]?.kind==='proposal')return state.compareTargetVersion;if(versionById[state.activeVersion]?.kind==='proposal')return state.activeVersion;return candidates.at(-1).id}
1243
+ function syncCanvasLayerControls(){const afterId=activeAfterVersion();if(!afterId){state.showBefore=true;state.showAfter=false;state.activeVersion=baselineVersion}document.querySelectorAll('[data-canvas-layer="findings"]').forEach(button=>button.setAttribute('aria-pressed',String(state.showFindings)));const count=$('.canvas-layer-count');if(count)count.textContent=String(findingsForCanvas().length);const comparing=Boolean(afterId&&state.showBefore&&state.showAfter),visibility=comparing?'both':state.showBefore?'before':'after',layout=$('.compare-layout-controls'),strip=$('.canvas-layer-controls');if(strip)strip.dataset.hasAfter=String(Boolean(afterId));document.querySelectorAll('[data-version-visibility]').forEach(button=>{button.setAttribute('aria-pressed',String(button.dataset.versionVisibility===visibility));button.disabled=!afterId&&button.dataset.versionVisibility!=='before'});if(layout)layout.setAttribute('aria-disabled',String(!comparing));document.querySelectorAll('[data-compare-mode]').forEach(button=>{button.disabled=!comparing;button.setAttribute('aria-pressed',String(comparing&&button.dataset.compareMode===state.compareMode))})}
1244
+ function toggleCanvasLayer(layer){if(layer!=='findings')return;state.showFindings=!state.showFindings;if(!state.showFindings)state.openFindingId=null;persist();syncCanvasLayerControls();renderFindingMarkers()}
1245
+ function setVersionVisibility(mode){const afterId=activeAfterVersion();if(!['before','both','after'].includes(mode)||(!afterId&&mode!=='before'))return;if(mode==='both'){if(state.view!=='compare')state.layerReturnView=state.view;state.showBefore=true;state.showAfter=true;state.compareBaseVersion=baselineVersion;state.compareTargetVersion=afterId;state.activeVersion=afterId;state.view='compare'}else{state.showBefore=mode==='before';state.showAfter=mode==='after';state.activeVersion=state.showBefore?baselineVersion:afterId;if(state.view==='compare')state.view=state.layerReturnView||'single'}if(versionSelect)versionSelect.value=state.activeVersion;persist();writeLocation(false);renderView()}
1246
+ function findingPinCard(item,number,x,y,stage){const width=280,height=230,viewportLeft=stage.scrollLeft+10,viewportTop=stage.scrollTop+10,viewportRight=stage.scrollLeft+stage.clientWidth-10,viewportBottom=stage.scrollTop+stage.clientHeight-10;let left=x+15,top=y-14;if(left+width>viewportRight)left=Math.max(viewportLeft,x-width-15);top=Math.max(viewportTop,Math.min(top,viewportBottom-height));return `<article class="finding-pin-card" data-finding-popover="${esc(item.id)}" style="left:${Math.round(left)}px;top:${Math.round(top)}px"><header class="finding-pin-card-header"><span class="finding-pin-number">${number}</span><h3 class="finding-pin-card-title">${esc(item.title)}</h3><button type="button" class="finding-pin-collapse" data-collapse-finding="${esc(item.id)}" aria-label="Свернуть описание проблемы" title="Свернуть в маркер">${iconMarkup('minus')}</button></header><div class="finding-pin-card-body"><div class="finding-pin-card-meta">${esc(item.severity)} · ${esc(item.category||'UI/UX')}</div><p>${esc(item.observation)}</p><p><b>Исправить:</b> ${esc(item.recommendation)}</p><div class="finding-pin-card-actions"><button type="button" class="primary" data-open-finding-list="${esc(item.id)}">В списке проблем</button>${item.proposalVersionId?`<button type="button" data-pin-compare="${esc(item.id)}">Сравнить</button>`:''}</div></div></article>`}
1247
+ function openFindingInList(id){state.findingFocus=[id];state.findingFilter='linked';setInspectorTab('review');state.reviewSection='problems';persist();renderReviewSections();renderFindings();requestAnimationFrame(()=>document.querySelector(`.finding-card[data-finding-id="${CSS.escape(id)}"]`)?.scrollIntoView({block:'center',behavior:'smooth'}))}
1248
+ let findingMarkerFrame=0;function scheduleFindingMarkers(){cancelAnimationFrame(findingMarkerFrame);findingMarkerFrame=requestAnimationFrame(()=>{findingMarkerFrame=0;renderFindingMarkers()})}
1249
+ function markerSideForDevice(anchor,device){if(state.view==='compare'){if(device.dataset.versionId===state.compareBaseVersion)return 'left';if(device.dataset.versionId===state.compareTargetVersion)return 'right'}const anchorRect=anchor.getBoundingClientRect(),deviceRect=device.getBoundingClientRect();return anchorRect.left+anchorRect.width/2<deviceRect.left+deviceRect.width/2?'left':'right'}
1250
+ function placeCanvasMarker(anchor,device,stage,stageRect,lanes,deviceKey){const rect=anchor.getBoundingClientRect(),deviceRect=device.getBoundingClientRect(),side=markerSideForDevice(anchor,device),step=28,edgeGap=12,anchorX=(side==='left'?rect.left:rect.right)-stageRect.left+stage.scrollLeft,anchorY=rect.top-stageRect.top+stage.scrollTop+Math.min(12,rect.height/2),top=deviceRect.top-stageRect.top+stage.scrollTop+12,bottom=deviceRect.bottom-stageRect.top+stage.scrollTop-12,desired=Math.max(top,Math.min(bottom,anchorY));let column=0,y=desired;for(;column<8;column+=1){const key=`${deviceKey}:${side}:${column}`,used=lanes.get(key)||[];let found=null;for(let radius=0;radius<32&&!found;radius+=1){for(const candidate of radius?[desired+radius*step,desired-radius*step]:[desired])if(candidate>=top&&candidate<=bottom&&used.every(value=>Math.abs(value-candidate)>=step-1)){found=candidate;break}}if(found!==null){y=found;used.push(y);lanes.set(key,used);break}}const edge=side==='left'?deviceRect.left:deviceRect.right,x=edge-stageRect.left+stage.scrollLeft+(side==='left'?-edgeGap-column*step:edgeGap+column*step);return {x,y,anchorX,anchorY,side}}
1251
+ function findingLeaderMarkup(item,deviceKey,position){if(!item.nodeId)return '';const dx=position.x-position.anchorX,dy=position.y-position.anchorY,distance=Math.hypot(dx,dy);if(distance<16)return '';return `<span class="finding-pin-leader" data-marker-device="${esc(deviceKey)}" style="left:${Math.round(position.anchorX)}px;top:${Math.round(position.anchorY)}px;width:${Math.round(distance)}px;transform:rotate(${Math.atan2(dy,dx)}rad)"></span>`}
1252
+ function annotationNumber(id){return state.annotations.findIndex(item=>item.id===id)+1}
1253
+ function annotationAppliesToVersion(item,versionId){return !item.versionId||item.versionId===versionId}
1254
+ function annotationPinCard(item,number,x,y,stage){const width=260,height=170,viewportLeft=stage.scrollLeft+10,viewportTop=stage.scrollTop+10,viewportRight=stage.scrollLeft+stage.clientWidth-10,viewportBottom=stage.scrollTop+stage.clientHeight-10;let left=x+15,top=y-14;if(left+width>viewportRight)left=Math.max(viewportLeft,x-width-15);top=Math.max(viewportTop,Math.min(top,viewportBottom-height));return `<article class="finding-pin-card annotation-pin-card" data-annotation-popover="${esc(item.id)}" style="left:${Math.round(left)}px;top:${Math.round(top)}px"><header class="finding-pin-card-header"><span class="finding-pin-number annotation-pin-number">${number}</span><h3 class="finding-pin-card-title">Комментарий</h3><button type="button" class="finding-pin-collapse" data-collapse-annotation="${esc(item.id)}" aria-label="Свернуть комментарий" title="Свернуть в маркер">${iconMarkup('minus')}</button></header><div class="finding-pin-card-body"><div class="finding-pin-card-meta">${esc(item.priority||'medium')} · ${esc(item.status||'new')}</div><p data-i18n-skip>${esc(item.text)}</p><div class="finding-pin-card-actions"><button type="button" class="primary" data-open-annotation-list="${esc(item.id)}">В списке комментариев</button></div></div></article>`}
1255
+ function renderFindingMarkers(){
1256
+ const stage=$('.stage');if(!stage)return;
1257
+ stage.querySelector('.finding-canvas-layer')?.remove();syncCanvasLayerControls();
1258
+ const visible=state.showFindings?findingsForCanvas():[],visibleAnnotations=state.annotations.filter(item=>!['resolved','rejected'].includes(item.status||'new'));if(!visible.length&&!visibleAnnotations.length)return;
1259
+ const layer=document.createElement('div');layer.className='finding-canvas-layer';layer.style.width=`${Math.max(stage.clientWidth,stage.scrollWidth)}px`;layer.style.height=`${Math.max(stage.clientHeight,stage.scrollHeight)}px`;stage.append(layer);
1260
+ const stageRect=stage.getBoundingClientRect(),lanes=new Map();let opened=false;
1261
+ for(const item of visible){
1262
+ const devices=[...stage.querySelectorAll(`.device[data-screen-id="${CSS.escape(item.screenId||'')}"]`)];
1263
+ for(const device of devices){
1264
+ if(!findingAppliesToVersion(item,device.dataset.versionId))continue;
1265
+ const anchor=item.nodeId?device.querySelector(`[data-node-id="${CSS.escape(item.nodeId)}"]`):device.querySelector('.device-content');if(!anchor)continue;
1266
+ const deviceKey=`${item.screenId}:${device.dataset.versionId}`,position=placeCanvasMarker(anchor,device,stage,stageRect,lanes,deviceKey),{x,y}=position,number=findingNumber(item.id),markerDevice=esc(`${deviceKey}:${position.side}`);layer.insertAdjacentHTML('beforeend',findingLeaderMarkup(item,deviceKey,position));
1267
+ if(state.openFindingId===item.id&&!opened){layer.insertAdjacentHTML('beforeend',findingPinCard(item,number,x,y,stage).replace('data-finding-popover=',`data-marker-device="${markerDevice}" data-finding-popover=`));opened=true}
1268
+ else layer.insertAdjacentHTML('beforeend',`<button type="button" class="finding-pin" data-marker-device="${markerDevice}" data-severity="${esc(item.severity||'medium')}" data-open-finding="${esc(item.id)}" style="left:${Math.round(x)}px;top:${Math.round(y)}px" aria-label="Проблема ${number}: ${esc(item.title)}" title="${esc(item.title)}">${number}</button>`)
1269
+ }
1270
+ }
1271
+ for(const item of visibleAnnotations){
1272
+ const devices=[...stage.querySelectorAll(`.device[data-screen-id="${CSS.escape(item.screenId||'')}"]`)];
1273
+ for(const device of devices){
1274
+ if(!annotationAppliesToVersion(item,device.dataset.versionId))continue;
1275
+ const anchor=item.nodeId?device.querySelector(`[data-node-id="${CSS.escape(item.nodeId)}"]`):device.querySelector('.device-content');if(!anchor)continue;
1276
+ const deviceKey=`${item.screenId}:${device.dataset.versionId}`,position=placeCanvasMarker(anchor,device,stage,stageRect,lanes,deviceKey),{x,y}=position,number=annotationNumber(item.id),markerDevice=esc(`${deviceKey}:${position.side}`);layer.insertAdjacentHTML('beforeend',findingLeaderMarkup(item,deviceKey,position));
1277
+ if(state.openAnnotationId===item.id&&!opened){layer.insertAdjacentHTML('beforeend',annotationPinCard(item,number,x,y,stage).replace('data-annotation-popover=',`data-marker-device="${markerDevice}" data-annotation-popover=`));opened=true}
1278
+ else layer.insertAdjacentHTML('beforeend',`<button type="button" class="annotation-pin" data-marker-device="${markerDevice}" data-open-annotation="${esc(item.id)}" style="left:${Math.round(x)}px;top:${Math.round(y)}px" aria-label="Комментарий ${number}: ${esc(item.text)}" title="${esc(item.text)}">${number}</button>`)
1279
+ }
1280
+ }
1281
+ layer.querySelectorAll('.finding-pin,.annotation-pin,.finding-pin-card').forEach(element=>element.addEventListener('pointerdown',event=>{if(event.button!==1)event.stopPropagation()}));
1282
+ layer.querySelectorAll('[data-open-finding]').forEach(button=>button.addEventListener('click',event=>{event.preventDefault();event.stopPropagation();state.openAnnotationId=null;state.openFindingId=button.dataset.openFinding;persist();scheduleFindingMarkers()}));
1283
+ layer.querySelectorAll('[data-collapse-finding]').forEach(button=>button.addEventListener('click',event=>{event.preventDefault();event.stopPropagation();state.openFindingId=null;persist();scheduleFindingMarkers()}));
1284
+ layer.querySelectorAll('[data-open-finding-list]').forEach(button=>button.addEventListener('click',event=>{event.stopPropagation();openFindingInList(button.dataset.openFindingList)}));
1285
+ layer.querySelectorAll('[data-pin-compare]').forEach(button=>button.addEventListener('click',event=>{event.stopPropagation();focusFinding(button.dataset.pinCompare,true)}));
1286
+ layer.querySelectorAll('[data-open-annotation]').forEach(button=>button.addEventListener('click',event=>{event.preventDefault();event.stopPropagation();state.openFindingId=null;state.openAnnotationId=button.dataset.openAnnotation;scheduleFindingMarkers()}));
1287
+ layer.querySelectorAll('[data-collapse-annotation]').forEach(button=>button.addEventListener('click',event=>{event.preventDefault();event.stopPropagation();state.openAnnotationId=null;scheduleFindingMarkers()}));
1288
+ layer.querySelectorAll('[data-open-annotation-list]').forEach(button=>button.addEventListener('click',event=>{event.stopPropagation();focusAnnotation(button.dataset.openAnnotationList)}))
1289
+ }
1290
+ function setFindingDecision(id,decision){state.findingDecisions[id]=decision;state.agentHandoff=null;persist();renderFindings()}
1291
+ function selectedFindings(){return findings.filter(item=>state.findingDecisions[item.id]==='accepted')}
1292
+ function renderRevisionNotice(){const badge=$('.revision-badge'),notice=$('.revision-notice');if(badge)badge.textContent=reviewRevision.split('-').at(-1).slice(0,6);if(notice)notice.hidden=!state.staleReview}
1293
+ function migrateReviewState(){const old=state.staleReview;if(!old)return;const compatiblePortable=(old.runtimeFindings||[]).filter(item=>!isRuntimeFinding(item)&&item?.id&&screens.some(screen=>screen.id===item.screenId)&&(!item.nodeId||nodes[item.nodeId])).map(item=>({...item,instances:(item.instances||[]).filter(id=>nodes[id])}));state.runtimeFindings=compatiblePortable;const validIds=new Set(findings.map(item=>item.id));state.findingDecisions=Object.fromEntries(Object.entries(old.findingDecisions||{}).filter(([id])=>validIds.has(id)));const annotations=(old.annotations||[]).filter(item=>(!item.screenId||screens.some(screen=>screen.id===item.screenId))&&(!item.nodeId||nodes[item.nodeId])&&(!item.versionId||versionById[item.versionId]));const annotationIds=new Set(state.annotations.map(item=>item.id));for(const item of annotations)if(!annotationIds.has(item.id))state.annotations.push(item);if(old.selectedNodeId&&nodes[old.selectedNodeId]&&screens.some(screen=>screen.id===old.selectedScreenId)){state.selectedNodeId=old.selectedNodeId;state.selectedScreenId=old.selectedScreenId}state.diagnostics=null;state.diagnosticTargetIds=[];const total=(old.annotations||[]).length+Object.keys(old.findingDecisions||{}).length+(old.runtimeFindings||[]).length,migrated=annotations.length+Object.keys(state.findingDecisions).length+compatiblePortable.length;state.staleReview=null;persist();renderView();showToast(`Перенесено ${migrated}; технические результаты будут рассчитаны заново`)}
1294
+ function clearRuntimeReview(){const removed=state.runtimeFindings.filter(isRuntimeFinding).map(item=>item.id);state.runtimeFindings=state.runtimeFindings.filter(item=>!isRuntimeFinding(item));for(const id of removed)delete state.findingDecisions[id];state.findingFocus=state.findingFocus.filter(id=>!removed.includes(id));if(removed.includes(state.focusedFindingId))state.focusedFindingId=null;if(removed.includes(state.openFindingId))state.openFindingId=null;state.diagnostics=null;state.diagnosticsProgress='';state.diagnosticTargetIds=[];return removed.length}
1295
+ function discardReviewState(){state.staleReview=null;persist();renderRevisionNotice();showToast('Начато чистое ревью текущей версии')}
1296
+ function approveActiveVersion(){const active=versionById[state.activeVersion];if(active?.kind!=='proposal')return;state.versionDecision='accepted';persist();renderQueue();renderFindings();renderFixQueue();showToast('Новый макет подтверждён; файлы проекта ещё не изменены')}
1297
+ function rejectActiveVersion(){if(versionById[state.activeVersion]?.kind!=='proposal')return;state.versionDecision='rejected';persist();renderQueue();renderFixQueue();showToast('Вариант отправлен на доработку')}
1298
+ function sourceTargetsFor(items){const targets=new Set(),root=String(ir.project?.root||'').replace(/[\\/]+$/,'');for(const item of items){const ids=[item.nodeId,...(item.instances||[])].filter(Boolean);for(const id of ids){const file=nodes[id]?.source?.file;if(file)targets.add(root?`${root}/${String(file).replace(/^[/\\]+/,'')}`:String(file))}}return [...targets]}
1299
+ function prepareSourceHandoff(){if(versionById[state.activeVersion]?.kind!=='proposal'||state.versionDecision!=='accepted')return;prepareAgentTask('implement')}
1300
+ function agentTaskContext(kind){const selected=selectedFindings(),openAnnotations=state.annotations.filter(item=>!['resolved','rejected'].includes(item.status||'new')).slice(0,30).map(item=>({id:item.id,screenId:item.screenId,nodeId:item.nodeId,versionId:item.versionId,text:item.text,priority:item.priority,status:item.status})),sourceChangeAllowed=kind==='implement',projectRoot=String(ir.project?.root||'').trim(),artifactDir=String(previewContext.artifactDir||'').trim(),sourceTargets=sourceTargetsFor(selected),activeVersion=kind==='expert'?baselineVersion:state.activeVersion,uiIrFile=artifactDir?`${artifactDir}/ui-ir.json`:'ui-ir.json',contextFile=artifactDir?`${artifactDir}/ui-agent-context.json`:'ui-agent-context.json',patchFile=artifactDir?`${artifactDir}/ui-ir.patch.json`:'ui-ir.patch.json';return {type:'ui-design-workbench-agent-job',version:3,provider:String(previewContext.handoffProvider||'generic'),kind,reviewRevision,project:ir.project?.name||'Project',projectRoot,artifactDir,uiIrFile,contextFile,patchFile,scope:state.reviewScope,screenIds:reviewScopeScreens().map(item=>item.id),activeVersion,reviewVersionId:baselineVersion,baselineVersion,approvedVersionId:state.versionDecision==='accepted'?state.activeVersion:null,acceptedFindingIds:selected.map(item=>item.id),findings:selected.map(item=>({id:item.id,title:item.title,severity:item.severity,screenId:item.screenId,nodeId:item.nodeId||null,reviewVersionId:findingReviewVersion(item),observation:item.observation,recommendation:item.recommendation,proposalVersionId:item.proposalVersionId||null,noProposalReason:item.noProposalReason||null})),annotations:openAnnotations,maxVariantCount:2,preferredVariantCount:1,sourceTargets,allowedWrites:sourceChangeAllowed?[projectRoot,...sourceTargets,artifactDir].filter(Boolean):['ui-ir.patch.json','ui-ir.proposed.json','ui-preview.html','*.report.json'],sourceChangeAllowed}}
1301
+ function agentTaskDescriptor(kind='expert'){const artifactDir=String(previewContext.artifactDir||'').trim(),context=agentTaskContext(kind),provider=String(previewContext.handoffProvider||'generic').toLowerCase(),implementation=kind==='implement';if(!artifactDir)return {supported:false,kind,error:'Каталог макета не записан в HTML'};if((kind==='proposal'||implementation)&&!context.acceptedFindingIds.length)return {supported:false,kind,error:'Сначала выберите проблемы для исправления'};if(implementation&&!context.projectRoot)return {supported:false,kind,error:'В макете не записан корень исходного проекта'};const task=implementation?'Исправь выбранные проблемы непосредственно в исходном проекте. HTML-вариант сам по себе не считается исправлением.':kind==='proposal'?'Создай sparse-вариант макета только для выбранных проблем.':'Проведи глубокое UI/UX-ревью immutable baseline в указанной области.',batchScreens=kind==='expert'?context.screenIds.slice(0,1):context.screenIds,scopeCommand=`uidw --repo "${context.projectRoot||'.'}" scope --ir "${context.uiIrFile}" ${batchScreens.map(id=>`--screen "${id}"`).join(' ')} ${context.acceptedFindingIds.map(id=>`--finding "${id}"`).join(' ')} --output "${context.contextFile}"`;const rules=implementation?`1. Используй contextFile и sourceTargets; не загружай полный ui-ir.json в контекст агента.\n2. Измени реальные UI-файлы проекта для всех acceptedFindingIds.\n3. После правок выполни инкрементальный uidw sync и только целевые проверки; не запускай новое полное ревью.\n4. Запиши targeted verification существующих finding ID и сообщи в чате изменённые файлы и нерешённые пункты.`:`1. Выполни команду scoped context ниже и читай только contextFile; не вставляй полный ui-ir.json в запрос. Для общего ревью повторяй scope по одному экрану из screenIds и объединяй только стабильные findings.\n2. Не изменяй исходный проект и immutable baseline. Верни изменения операциями в patchFile формата ui-ir.patch.json.\n3. Для selected findings создай один сдержанный sparse proposal; второй — только при реальном UX-компромиссе.\n4. Примени патч командой uidw patch apply, пересобери preview и выполни целевые projection/headless проверки.\n5. В чате выведи нумерованный список результатов; не дублируй его обычным текстовым блоком в HTML.\n\nПервая команда контекста:\n${scopeCommand}`;const prompt=`Используй ui-design-workbench. ${task}\n\nКомпактный маршрут:\n${JSON.stringify(context)}\n\nОбязательный результат:\n${rules}`;const params=new URLSearchParams({prompt,path:implementation?context.projectRoot:artifactDir}),url=provider==='codex'?`codex://new?${params.toString()}`:null;return {supported:true,kind,path:implementation?context.projectRoot:artifactDir,prompt,url,provider,context}}
1302
+ async function prepareAgentTask(kind){const descriptor=agentTaskDescriptor(kind);if(!descriptor.supported){showToast(descriptor.error);if(kind==='proposal')downloadJson('ui-fix-request.json',fixRequestPayload());else if(kind==='implement')downloadJson('ui-source-change-request.json',sourceRequestPayload());else if(state.diagnostics)downloadJson('ui-expert-review-request.json',expertReviewRequestPayload());return}state.agentHandoff={kind,provider:descriptor.provider,status:'prepared',createdAt:new Date().toISOString(),acceptedFindingIds:descriptor.context.acceptedFindingIds,screenIds:descriptor.context.screenIds,artifactDir:descriptor.path};if(kind==='implement')state.sourcePreparedVersionId=state.activeVersion||baselineVersion;persist();renderAgentHandoff();renderReviewNextAction();if(descriptor.url){showToast(kind==='implement'?'Задача на исправление проекта подготовлена':'Задача подготовлена — отправьте запрос в агенте');location.href=descriptor.url;return}try{await navigator.clipboard.writeText(descriptor.prompt);showToast(kind==='implement'?'Задание на исправление проекта скопировано':'Запрос для AI-агента скопирован')}catch(_){downloadJson('ui-agent-job.json',{...descriptor.context,prompt:descriptor.prompt});showToast('Задание для AI-агента скачано')}}
1303
+ async function copyAgentTask(){const kind=state.agentHandoff?.kind||'expert',descriptor=agentTaskDescriptor(kind);if(!descriptor.supported){showToast(descriptor.error);return}try{await navigator.clipboard.writeText(descriptor.prompt);showToast('Запрос для AI-агента скопирован')}catch(_){showToast('Не удалось скопировать запрос')}}
1304
+ function refreshPreview(){location.reload()}
1305
+ function renderAgentHandoff(){renderVersionStatus();const panel=$('.agent-handoff-panel'),status=$('.agent-handoff-status'),meta=$('.agent-handoff-meta');if(!panel||!status||!meta)return;const value=state.agentHandoff;panel.hidden=!value;if(!value)return;status.textContent=value.kind==='proposal'?'Новый макет готовится':value.kind==='implement'?'Задание на применение подготовлено':'Углублённый анализ подготовлен';meta.textContent=value.kind==='implement'?`${new Date(value.createdAt).toLocaleString(workbenchLocale)} · выполните задание в агенте, затем обновите макет для целевой проверки.`:`${new Date(value.createdAt).toLocaleString(workbenchLocale)} · выполните подготовленный запрос в выбранном AI-агенте. После завершения вернитесь сюда и обновите макет.`}
1306
+ function renderFixQueue(){const selected=selectedFindings(),stats=$('.fix-queue-stats'),copy=$('.copy-fix-request'),previewItems=selected.filter(item=>!item.noProposalReason),addressed=previewItems.filter(item=>findingAddressed(item)).length,verified=selected.filter(findingResolved).length,direct=selected.length-previewItems.length;document.querySelectorAll('.fix-queue-count').forEach(count=>count.textContent=String(selected.length));document.querySelectorAll('.finding-selection-count').forEach(count=>{count.textContent=selected.length?`Выбрано: ${selected.length}`:'Ничего не выбрано';count.classList.toggle('has-selection',selected.length>0)});if(!stats)return;stats.innerHTML=selected.length?`<span class="fix-queue-stat">Выбрано ${selected.length}</span><span class="fix-queue-stat">Учтено в новом макете ${addressed}/${previewItems.length}</span><span class="fix-queue-stat">Применено и проверено ${verified}/${selected.length}</span>${direct?`<span class="fix-queue-stat">Без макета ${direct}</span>`:''}`:'<span class="fix-queue-stat">Сначала выберите проблемы на шаге 2</span>';if(copy){copy.disabled=!selected.length;copy.innerHTML=`${iconMarkup('copy')}<span>${selected.length?`Скопировать · ${selected.length}`:'Скопировать задание'}</span>`}renderReviewNextAction();renderReviewSections()}
1307
+ function selectedProposalId(){const selected=selectedFindings();if(!selected.length)return null;const matches=proposalVersions().filter(version=>{const addressed=resolvedFindingIdsForVersion(version.id);return selected.every(item=>findingResolved(item)||addressed.has(item.id))});return matches.at(-1)?.id||null}
1308
+ function compareSelectedProposal(){const id=selectedProposalId();if(!id)return;state.activeVersion=id;state.compareTargetVersion=id;versionSelect.value=id;state.versionDecision='pending';renderCompareVersionOptions();persist();setReviewSection('changes');setView('compare')}
1309
+ function renderReviewNextAction(){
1310
+ const button=$('.review-next-action'),status=$('.review-context-status'),reject=$('.review-reject-action');if(!button||!status)return;
1311
+ const selected=selectedFindings(),active=versionById[state.activeVersion],activeProposal=active?.kind==='proposal',previewItems=selected.filter(item=>!item.noProposalReason),missing=previewItems.filter(item=>!findingResolved(item)&&!findingAddressed(item)),approved=activeProposal&&state.versionDecision==='accepted',allVerified=selected.length>0&&selected.every(findingResolved),handoff=state.agentHandoff?.status==='prepared'?state.agentHandoff?.kind:null;
1312
+ button.disabled=state.diagnosticsRunning;reject.hidden=state.reviewSection!=='changes'||!selected.length||state.view!=='compare'||!activeProposal||approved;reject.onclick=rejectActiveVersion;
1313
+ if(state.diagnosticsRunning){button.dataset.action='wait';button.textContent='Проверяем интерфейс…';status.textContent=state.diagnosticsProgress||'Проверка выполняется'}
1314
+ else if(!state.diagnostics){button.dataset.action='run';button.textContent='Проверить интерфейс';status.textContent='Начните с проверки — она найдёт и объяснит проблемы.'}
1315
+ else if(!findings.length){button.dataset.action='run';button.textContent='Проверить ещё раз';status.textContent='Проверка завершена, проблем не найдено'}
1316
+ else if(!selected.length&&state.reviewSection==='summary'){button.dataset.action='problems';button.textContent=`Посмотреть проблемы · ${findings.length}`;status.textContent=`Проверка завершена · найдено проблем: ${findings.length}.`}
1317
+ else if(!selected.length&&state.reviewSection==='problems'){button.dataset.action='select-all';button.textContent=`Выбрать все · ${visibleFindingIds().length||findings.length}`;status.textContent='Ничего не выбрано. Можно выбрать отдельные проблемы в списке или все сразу.'}
1318
+ else if(!selected.length){button.dataset.action='problems';button.textContent='Вернуться к проблемам';status.textContent='Для продолжения выберите хотя бы одну проблему на шаге 2.'}
1319
+ else if(state.reviewSection==='summary'){button.dataset.action='problems';button.textContent=`К выбранным проблемам · ${selected.length}`;status.textContent='Продолжите со шага 2, чтобы проверить выбор.'}
1320
+ else if(state.reviewSection==='problems'){button.dataset.action='changes';button.textContent=`Перейти к исправлению · ${selected.length}`;status.textContent=`Выбрано проблем: ${selected.length}. Следующий шаг — подготовить исправления.`}
1321
+ else if(allVerified){button.dataset.action='done';button.textContent='Применено и проверено';button.disabled=true;status.textContent=`Проверка подтверждает исправление выбранных проблем: ${selected.length}.`}
1322
+ else if(handoff==='implement'){button.dataset.action='refresh';button.textContent='Обновить после применения';status.textContent='Задание на изменение проекта подготовлено. Выполните его, затем обновите макет.'}
1323
+ else if(handoff==='proposal'){button.dataset.action='refresh';button.textContent='Проверить готовый вариант';status.textContent=`Готовится новый макет для ${selected.length} выбранных проблем.`}
1324
+ else if(selectedProposalId()&&state.activeVersion!==selectedProposalId()){button.dataset.action='compare-selected';button.textContent=`Проверить новый макет · ${selected.length}`;status.textContent='Для выбранных проблем уже подготовлен новый макет.'}
1325
+ else if(!activeProposal||missing.length){button.dataset.action='proposal';button.textContent=`Подготовить новый макет · ${selected.length}`;status.textContent='Сначала изменится только макет. Файлы проекта останутся без изменений.'}
1326
+ else if(state.view!=='compare'){button.dataset.action='compare-active';button.textContent=`Сравнить с исходным · ${selected.length}`;status.textContent='Проверьте, как выбранные проблемы учтены в новом макете.'}
1327
+ else if(!approved){button.dataset.action='approve';button.textContent='Подтвердить новый макет';status.textContent='Подтверждение макета ещё не изменяет файлы проекта.'}
1328
+ else{button.dataset.action='source';button.textContent=`Применить исправления · ${selected.length}`;status.textContent=`Будут изменены файлы проекта только для выбранных проблем: ${selected.length}.`}
1329
+ }
1330
+ function handleReviewNextAction(){const action=$('.review-next-action')?.dataset.action;if(action==='run')runAutomatedReview();else if(action==='problems')setReviewSection('problems');else if(action==='select-all')acceptVisibleFindings();else if(action==='changes')setReviewSection('changes');else if(action==='proposal'){state.agentHandoff=null;prepareAgentTask('proposal')}else if(action==='compare-active'){setView('compare');renderReviewNextAction()}else if(action==='compare-selected')compareSelectedProposal();else if(action==='refresh')refreshPreview();else if(action==='approve')approveActiveVersion();else if(action==='source'){const count=selectedFindings().length;if(confirm(`Применить ${count} выбранных исправлений к реальным файлам проекта?`))prepareSourceHandoff()}}
1331
+ function focusFinding(id,compare=false){const item=findings.find(value=>value.id===id);if(!item)return;const reviewVersion=findingReviewVersion(item),proposalId=findingProposalId(item);setInspectorTab('review');state.reviewSection=compare?'changes':'problems';renderReviewSections();state.focusedFindingId=id;state.diagnosticTargetIds=[];if(compare&&proposalId){state.compareBaseVersion=reviewVersion;state.activeVersion=proposalId;state.compareTargetVersion=proposalId;versionSelect.value=state.activeVersion;renderCompareVersionOptions();persist();setScreen(item.screenId,'compare');showToast('Исходный и новый макеты открыты для сравнения');return}state.activeVersion=reviewVersion;state.compareBaseVersion=reviewVersion;versionSelect.value=reviewVersion;renderCompareVersionOptions();persist();setScreen(item.screenId,'single');requestAnimationFrame(()=>{const element=item.nodeId?document.querySelector(`[data-node-id="${CSS.escape(item.nodeId)}"]`):null;if(!element){showToast('Открыт исходный экран; точный элемент не указан');return}element.dataset.findingSeverity=item.severity||'medium';element.scrollIntoView({block:'center',inline:'center'});showToast('Проблема показана в исходном макете')})}
1332
+ function auditFindingIds(item){return [...new Set((Array.isArray(item.findingIds)?item.findingIds:[]).filter(id=>findings.some(finding=>finding.id===id)))]}
1333
+ function auditLinksMarkup(item){const ids=auditFindingIds(item);if(!ids.length)return '<span class="audit-depth-unlinked">Нет отдельной карточки исправления</span>';const ready=ids.filter(id=>findingProposalId(findings.find(finding=>finding.id===id))).length,encoded=esc(ids.join(','));return `<div class="audit-depth-links"><button type="button" class="audit-depth-action primary" data-audit-findings="${encoded}">Показать проблемы · ${ids.length}</button><button type="button" class="audit-depth-action" data-accept-findings="${encoded}">Выбрать для применения</button>${ready?`<span class="audit-depth-unlinked">Учтено в новом макете: ${ready}</span>`:''}</div>`}
1334
+ function auditDepthMarkup(){const checks=Array.isArray(expertAudit.interactionChecks)?expertAudit.interactionChecks:[],layouts=Array.isArray(expertAudit.layoutChecks)?expertAudit.layoutChecks:[],ux=Array.isArray(expertAudit.uxAssessment)?expertAudit.uxAssessment:[];const item=(value,label,statusAttr='result')=>`<li class="audit-depth-item" data-${statusAttr}="${esc(value[statusAttr]||'not-run')}"><div class="audit-depth-title"><b>${esc(label)}</b><span>${esc(value[statusAttr]||'not-run')}</span></div><div>${esc(value.observed||value.observation||value.expected||'')}</div>${auditLinksMarkup(value)}</li>`;const checkMarkup=checks.length?`<details class="audit-depth"><summary>Проверки взаимодействий · ${checks.length}</summary><ul class="audit-depth-list">${checks.map(value=>item(value,value.id||'сценарий')).join('')}</ul></details>`:'';const layoutMarkup=layouts.length?`<details class="audit-depth"><summary>Типографика и геометрия · ${layouts.length}</summary><ul class="audit-depth-list">${layouts.map(value=>item(value,value.kind||'layout')).join('')}</ul></details>`:'';const uxMarkup=ux.length?`<details class="audit-depth"><summary>Отдельный UX-аудит · ${ux.length}</summary><ul class="audit-depth-list">${ux.map(value=>item(value,value.lens||'UX','status')).join('')}</ul></details>`:'';return checkMarkup+layoutMarkup+uxMarkup}
1335
+ const nextPaintIn=win=>new Promise(resolve=>win.requestAnimationFrame(()=>win.requestAnimationFrame(resolve)));const nextPaint=()=>nextPaintIn(window);
1336
+ function diagnosticProfiles(){return diagnosticsConfig.profiles?.length?diagnosticsConfig.profiles:[{id:'current',label:'Текущее окно',viewport:'current',zoomLevels:[.2,1,2]}]}
1337
+ async function createDiagnosticContext(profile){if(profile.viewport==='current'||!profile.viewport?.width||!profile.viewport?.height)return {win:window,doc:document,cleanup(){}};const frame=document.createElement('iframe');frame.setAttribute('aria-hidden','true');frame.tabIndex=-1;Object.assign(frame.style,{position:'fixed',left:'-20000px',top:'0',width:`${profile.viewport.width}px`,height:`${profile.viewport.height}px`,border:'0',opacity:'0',pointerEvents:'none',zIndex:'-1'});const source='<!doctype html>'+document.documentElement.outerHTML;const loaded=new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error(`Не удалось открыть sandbox ${profile.label}`)),10000);frame.addEventListener('load',()=>{clearTimeout(timer);resolve()},{once:true})});frame.srcdoc=source;document.body.append(frame);await loaded;await nextPaintIn(frame.contentWindow);if(!frame.contentWindow?.__uiPreviewDiagnostics)throw new Error(`Диагностический API недоступен в профиле ${profile.label}`);return {win:frame.contentWindow,doc:frame.contentDocument,cleanup(){frame.remove()}}}
1338
+ async function withDiagnosticProfile(profile,task){const context=await createDiagnosticContext(profile);try{return await task(context)}finally{context.cleanup()}}
1339
+ function diagnosticsScenario(kind){return (diagnosticsConfig.scenarios||[]).find(item=>item.kind===kind)}
1340
+ function diagnosticCheck(results,scenarioId,result,title,message,metrics={},screenId=null,severity='medium'){if(screenId&&state.reviewScope==='current'&&screenId!==state.screen)return;results.push({id:`runtime-${scenarioId}-${results.length+1}`,scenarioId,result,severity,title,message,metrics,screenId,createdAt:new Date().toISOString()})}
1341
+ function rectOverlap(a,b,tolerance=1){const width=Math.min(a.right,b.right)-Math.max(a.left,b.left),height=Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top);return width>tolerance&&height>tolerance?Math.round(width*height):0}
1342
+ function nodeLabel(el){return el.dataset?.nodeId||el.getAttribute('aria-label')||el.textContent?.trim().slice(0,40)||el.tagName.toLowerCase()}
1343
+ function visibleElement(el){const style=el.ownerDocument.defaultView.getComputedStyle(el),rect=el.getBoundingClientRect();return style.display!=='none'&&style.visibility!=='hidden'&&Number(style.opacity)!==0&&rect.width>0&&rect.height>0}
1344
+ function isInteractiveElement(el){return el?.dataset?.interactive==='true'}
1345
+ function interactiveElements(root){return [...root.querySelectorAll('*')].filter(isInteractiveElement)}
1346
+ function accessibleName(el){const doc=el.ownerDocument,labelled=el.getAttribute('aria-labelledby');if(labelled){const value=labelled.split(/\s+/).map(id=>doc.getElementById(id)?.textContent?.trim()||'').filter(Boolean).join(' ');if(value)return value}const own=el.getAttribute('aria-label')||el.getAttribute('title');if(own?.trim())return own.trim();if(el.labels?.length)return [...el.labels].map(label=>label.textContent?.trim()||'').filter(Boolean).join(' ');if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'||el.tagName==='SELECT')return '';return el.textContent?.replace(/\s+/g,' ').trim()||''}
1347
+ function textLineCount(el){if(!el.textContent?.trim())return 0;const range=el.ownerDocument.createRange();range.selectNodeContents(el);const baselines=[];for(const rect of range.getClientRects()){if(rect.width<.5||rect.height<.5)continue;if(!baselines.some(bottom=>Math.abs(bottom-rect.bottom)<2.5))baselines.push(rect.bottom)}return baselines.length}
1348
+ function compactList(items,limit=5){const labels=items.slice(0,limit).map(nodeLabel);return labels.join(', ')+(items.length>limit?` +${items.length-limit}`:'')}
1349
+ function setDiagnosticsProgress(message){state.diagnosticsProgress=message;renderDiagnostics();renderReviewLauncher()}
1350
+ function diagnosticNodeIds(item){const values=[],push=value=>{if(Array.isArray(value))value.forEach(push);else if(typeof value==='string'&&nodes[value])values.push(value)};for(const key of ['overlaps','overflow','multiline','smallTargets','paddingVariance','unnamed','symbolic'])push(item.metrics?.[key]);return [...new Set(values)]}
1351
+ function diagnosticFindingId(checkId){return `runtime-finding-${checkId}`}
1352
+ function diagnosticFindingData(check){const id=diagnosticFindingId(check.id),screenId=screens.some(screen=>screen.id===check.screenId)?check.screenId:(state.screen||screens[0]?.id),nodeIds=diagnosticNodeIds(check);return {id,title:`Диагностика: ${check.title}`,category:'runtime-diagnostic',severity:['high','medium','low'].includes(check.severity)?check.severity:'medium',confidence:'high',screenId,nodeId:nodeIds[0]||null,observation:check.message,impact:'Измеренная ошибка может нарушать читаемость, управление или устойчивость макета в проверенном состоянии.',recommendation:'Исправить перечисленные узлы и повторять автоматическую проверку до результата pass.',evidence:[{type:'heuristic',ref:`runtime-diagnostics.${check.scenarioId}`,note:`Автоматически измерено ${check.createdAt||'в текущем прогоне'}.`}],effort:'medium',noProposalReason:'Нужна новая proposal-версия по результату runtime-диагностики.',status:'open',runtimeDiagnosticId:check.id,instances:nodeIds}}
1353
+ function diagnosticGroupKey(check){return `${check.scenarioId}:${check.screenId||'workbench'}`}
1354
+ function groupDiagnosticChecks(checks){const groups=new Map();for(const check of checks){const key=diagnosticGroupKey(check);if(!groups.has(key))groups.set(key,[]);groups.get(key).push(check)}return [...groups.values()]}
1355
+ function diagnosticGroupData(checks){const first=checks[0],screenId=screens.some(screen=>screen.id===first.screenId)?first.screenId:(state.screen||screens[0]?.id),scenario=(diagnosticsConfig.scenarios||[]).find(item=>item.id===first.scenarioId),nodeIds=[...new Set(checks.flatMap(diagnosticNodeIds))],rank={high:3,medium:2,low:1},severity=checks.reduce((best,item)=>(rank[item.severity]||0)>(rank[best]||0)?item.severity:best,'low'),id=`runtime-group-${revisionHash(diagnosticGroupKey(first))}`;return {id,title:`Автопроверка: ${scenario?.label||first.title}${first.screenId?` · ${screens.find(item=>item.id===first.screenId)?.name||first.screenId}`:''}`,category:'runtime-diagnostic',severity,confidence:'high',screenId,nodeId:nodeIds[0]||null,reviewVersionId:baselineVersion,observation:`Непрошедших проверок в группе: ${checks.length}. ${checks.slice(0,3).map(item=>item.message).join(' ')}`,impact:'Повторяющаяся измеренная ошибка может нарушать читаемость, управление или устойчивость макета в нескольких состояниях.',recommendation:'Исправить общую причину для перечисленных состояний и повторять автоматическую проверку до результата pass.',evidence:checks.slice(0,12).map(item=>({type:'heuristic',ref:`runtime-diagnostics.${item.id}`,note:item.message})),effort:checks.length>4?'large':'medium',noProposalReason:'Нужна новая proposal-версия по результату runtime-диагностики.',status:'open',systemic:checks.length>1,runtimeDiagnosticId:first.id,runtimeDiagnosticIds:checks.map(item=>item.id),instances:nodeIds}}
1356
+ function createDiagnosticFinding(check,accept=false){setInspectorTab('review');const id=diagnosticFindingId(check.id),existing=findings.find(item=>item.id===id);if(!existing){const screenId=screens.some(screen=>screen.id===check.screenId)?check.screenId:(state.screen||screens[0]?.id),nodeIds=diagnosticNodeIds(check);state.runtimeFindings.push({id,title:`Диагностика: ${check.title}`,category:'runtime-diagnostic',severity:['high','medium','low'].includes(check.severity)?check.severity:'medium',confidence:'high',screenId,nodeId:nodeIds[0]||null,reviewVersionId:baselineVersion,observation:check.message,impact:'Измеренная ошибка может нарушать читаемость, управление или устойчивость макета в проверенном состоянии.',recommendation:'Исправить перечисленные узлы и повторять автоматическую проверку до результата pass.',evidence:[{type:'heuristic',ref:`runtime-diagnostics.${check.scenarioId}`,note:`Автоматически измерено ${check.createdAt||'в текущем прогоне'}.`}],effort:'medium',noProposalReason:'Нужна новая proposal-версия по результату runtime-диагностики.',status:'open',runtimeDiagnosticId:check.id,instances:nodeIds})}if(accept)state.findingDecisions[id]='accepted';state.findingFocus=[id];state.findingFilter='linked';persist();renderFindings();renderDiagnostics();$('.automated-review')?.scrollIntoView({block:'start',behavior:'smooth'});showToast(accept?'Проблема добавлена в исправление':'Создана карточка проблемы')}
1357
+ function renderReviewLauncher(){const root=$('.review-launcher'),handoffs=document.querySelectorAll('.export-review-request'),agentButtons=document.querySelectorAll('.open-agent-review'),status=$('.review-launcher-status'),meta=$('.review-launcher-meta'),scope=$('.review-scope-select');if(!root||!status)return;const report=state.diagnostics,actionable=(report?.checks||[]).filter(isActionableDiagnostic),groups=groupDiagnosticChecks(actionable),targetScreens=reviewScopeScreens(),variants=stateVariantCases(targetScreens),verified=findings.filter(findingResolved).length,open=Math.max(0,findings.length-verified);root.dataset.state=state.diagnosticsRunning?'running':report?'complete':'idle';for(const handoff of handoffs)handoff.disabled=state.diagnosticsRunning||!report;for(const agentButton of agentButtons)agentButton.disabled=state.diagnosticsRunning||!report;if(scope){scope.value=state.reviewScope;scope.disabled=state.diagnosticsRunning}if(meta)meta.textContent=report?`${report.screenIds?.length||targetScreens.length} экранов · ${report.profiles?.length||diagnosticProfiles().length} размера окна · проверен исходный макет`:`Будет проверено: ${targetScreens.length} экранов и ${variants.length} состояний · исходный макет`;status.textContent=state.diagnosticsRunning?(state.diagnosticsProgress||'Подготовка проверки…'):!report?'Нажмите «Проверить интерфейс» внизу панели.':findings.length?(open?`Найдено проблем: ${findings.length}. Ещё не применены и не проверены: ${open}.`:`Все ${verified} проблем применены и проверены.`):groups.length?`Автопроверка нашла групп проблем: ${groups.length}.`:'Проверка завершена: проблем не найдено.';renderCoverage();renderAgentHandoff();renderReviewSections()}
1358
+ async function runAutomatedReview(){if(state.diagnosticsRunning)return;setInspectorTab('review');state.reviewSection='summary';renderReviewSections();state.findingFocus=[];await runDiagnostics('review');const actionable=(state.diagnostics?.checks||[]).filter(isActionableDiagnostic),groups=groupDiagnosticChecks(actionable),ids=[],previousRuntime=new Map(state.runtimeFindings.filter(isRuntimeFinding).map(item=>[item.id,item])),portableFindings=state.runtimeFindings.filter(item=>!isRuntimeFinding(item)),nextRuntime=[];for(const checks of groups){const data=diagnosticGroupData(checks),existing=previousRuntime.get(data.id),checkIds=new Set(data.runtimeDiagnosticIds||[data.runtimeDiagnosticId]),related=[...previousRuntime.values()].filter(item=>(item.runtimeDiagnosticIds||[item.runtimeDiagnosticId]).some(id=>checkIds.has(id))),accepted=related.some(item=>state.findingDecisions[item.id]==='accepted');if(existing){Object.assign(existing,data);nextRuntime.push(existing)}else nextRuntime.push(data);if(accepted)state.findingDecisions[data.id]='accepted';ids.push(data.id)}const removedRuntimeIds=[...previousRuntime.keys()].filter(id=>!ids.includes(id));state.runtimeFindings=[...portableFindings,...nextRuntime];for(const id of removedRuntimeIds)delete state.findingDecisions[id];state.focusedFindingId=ids.includes(state.focusedFindingId)?state.focusedFindingId:null;state.openFindingId=ids.includes(state.openFindingId)?state.openFindingId:null;const stateCases=(state.diagnostics?.checks||[]).filter(item=>item.scenarioId==='state-matrix'&&item.metrics?.stateNodeId).length;state.reviewRuns.push({id:`run-${Date.now()}`,createdAt:new Date().toISOString(),engineVersion:diagnosticsEngineVersion,sessionName:state.reviewSessionName,scope:state.reviewScope,scopeLabel:state.reviewScope==='all'?'Все экраны':screens.find(item=>item.id===state.screen)?.name||'Текущий экран',screenIds:state.diagnostics?.screenIds||[],checks:state.diagnostics?.checks?.length||0,stateCases,issues:ids.length,summary:state.diagnostics?.summary||{}});state.reviewRuns=state.reviewRuns.slice(-20);state.findingFocus=ids;state.findingFilter=ids.length?'linked':'all';const openFindings=findings.filter(item=>!findingResolved(item));state.reviewSection=openFindings.length?'problems':'summary';persist();renderFindings();renderDiagnostics();renderReviewLauncher();renderReviewHistory();renderReviewSections();showToast(openFindings.length?`Проверка завершена: проблем ${openFindings.length}`:'Проверка завершена: проблем не найдено')}
1359
+ function focusDiagnostic(check){const ids=diagnosticNodeIds(check);state.diagnosticTargetIds=ids;if(check.screenId&&screens.some(screen=>screen.id===check.screenId))setScreen(check.screenId,'single');else renderView();requestAnimationFrame(()=>{for(const id of ids)document.querySelectorAll(`[data-node-id="${CSS.escape(id)}"]`).forEach(node=>node.dataset.diagnosticTarget='true');document.querySelector('[data-diagnostic-target=true]')?.scrollIntoView({block:'center',inline:'center'});persist();showToast(ids.length?`Подсвечено элементов: ${ids.length}`:'Для проверки нет точной привязки к узлу')})}
1360
+ function renderDiagnostics(){const status=$('.diagnostics-status'),progress=$('.diagnostics-progress'),summary=$('.diagnostics-summary'),list=$('.diagnostics-results'),run=$('.run-diagnostics'),clear=$('.clear-diagnostics'),reportNode=$('#runtime-diagnostics-report');if(!status||!list)return;document.documentElement.dataset.diagnosticsStatus=state.diagnosticsRunning?'running':state.diagnostics?'complete':'idle';if(reportNode)reportNode.textContent=JSON.stringify(state.diagnostics);run.disabled=state.diagnosticsRunning;clear.disabled=state.diagnosticsRunning;progress.textContent=state.diagnosticsProgress||'';status.className='diagnostics-status';if(state.diagnosticsRunning){status.textContent='выполняется';status.classList.add('running')}else if(!state.diagnostics){status.textContent='не запускалась'}else{const failed=state.diagnostics.summary?.fail||0;status.textContent=failed?`ошибок ${failed}`:'пройдено';status.classList.add(failed?'fail':'pass')}if(!state.diagnostics){summary.innerHTML='';list.innerHTML='<p class="diagnostics-empty">Проверка ещё не запускалась.</p>';return}const counts=state.diagnostics.summary||{};summary.innerHTML=`<span class="diagnostics-count">Пройдено ${counts.pass||0}</span><span class="diagnostics-count">Предупреждения ${counts.warning||0}</span><span class="diagnostics-count">Ошибки ${counts.fail||0}</span>`;list.innerHTML=(state.diagnostics.checks||[]).map(item=>{const actionable=state.diagnostics?.mode==='review'&&item.result!=='pass',created=findings.some(finding=>finding.runtimeDiagnosticId===item.id||finding.runtimeDiagnosticIds?.includes(item.id)),targets=diagnosticNodeIds(item).length;return `<article class="diagnostic-card" data-result="${esc(item.result)}"><b>${esc(item.title)}</b><div>${esc(item.message)}</div><div class="diagnostic-meta">${esc(item.screenId||'workbench')} · ${esc(item.result)} · ${esc(item.severity)}${targets?` · узлов ${targets}`:''}</div>${actionable?`<div class="diagnostic-card-actions">${targets?`<button type="button" class="diagnostic-card-action" data-focus-diagnostic="${esc(item.id)}">${iconMarkup('inspect')}Показать</button>`:''}<button type="button" class="diagnostic-card-action" data-create-diagnostic="${esc(item.id)}" ${created?'disabled':''}>${created?'Создано':'Создать проблему'}</button><button type="button" class="diagnostic-card-action primary" data-accept-diagnostic="${esc(item.id)}">${iconMarkup('check')}В исправление</button></div>`:''}</article>`}).join('')||'<p class="diagnostics-empty">Нет результатов.</p>';list.querySelectorAll('[data-focus-diagnostic]').forEach(button=>button.addEventListener('click',()=>{const item=state.diagnostics.checks.find(check=>check.id===button.dataset.focusDiagnostic);if(item)focusDiagnostic(item)}));list.querySelectorAll('[data-create-diagnostic]').forEach(button=>button.addEventListener('click',()=>{const item=state.diagnostics.checks.find(check=>check.id===button.dataset.createDiagnostic);if(item)createDiagnosticFinding(item)}));list.querySelectorAll('[data-accept-diagnostic]').forEach(button=>button.addEventListener('click',()=>{const item=state.diagnostics.checks.find(check=>check.id===button.dataset.acceptDiagnostic);if(item)createDiagnosticFinding(item,true)}))}
1361
+ async function runZoomResetDiagnostic(results,scenario){state.view='overview';state.zoomMode='manual';state.zoom=.65;renderView();await nextPaint();setZoom(1);await nextPaint();const canvas=$('.overview-canvas'),label=$('.zoom-label')?.textContent?.trim(),transform=canvas?.style.transform||'';const ok=label==='100%'&&Math.abs(state.computedZoom-1)<.001&&/scale\(1\)/.test(transform);diagnosticCheck(results,scenario.id,ok?'pass':'fail','Сброс масштаба',ok?'Холст и индикатор синхронно вернулись к 100%.':`Состояния расходятся: label=${label||'—'}, zoom=${state.computedZoom}, transform=${transform||'—'}.`,{label,computedZoom:state.computedZoom,transform},null,ok?'low':'high')}
1362
+ async function runOverviewGeometryDiagnostic(results,scenario){for(const profile of diagnosticProfiles())await withDiagnosticProfile(profile,async({win,doc})=>{const api=win.__uiPreviewDiagnostics;api.apply({view:'overview',zoom:1});await nextPaintIn(win);let baseline=null,maxDrift=0,overlaps=0,shellMismatch=0,labelMismatch=0;for(const rawZoom of profile.zoomLevels||[.2,1,2]){const zoom=clampZoom(Number(rawZoom)||1);api.apply({view:'overview',zoom});await nextPaintIn(win);const canvas=doc.querySelector('.overview-canvas'),shell=doc.querySelector('.overview-canvas-shell'),cards=[...doc.querySelectorAll('.screen-card')];if(!canvas||!shell)continue;const canvasRect=canvas.getBoundingClientRect(),positions=cards.map(card=>{const rect=card.getBoundingClientRect();return {id:card.dataset.screenCard,left:(rect.left-canvasRect.left)/zoom,top:(rect.top-canvasRect.top)/zoom,width:rect.width/zoom,height:rect.height/zoom,rect}});if(!baseline)baseline=positions;else for(const item of positions){const first=baseline.find(value=>value.id===item.id);if(first)maxDrift=Math.max(maxDrift,Math.abs(first.left-item.left),Math.abs(first.top-item.top),Math.abs(first.width-item.width),Math.abs(first.height-item.height))}for(let i=0;i<positions.length;i++)for(let j=i+1;j<positions.length;j++)if(rectOverlap(positions[i].rect,positions[j].rect)>0)overlaps+=1;const expectedWidth=Math.ceil(canvas.scrollWidth*zoom),expectedHeight=Math.ceil(canvas.scrollHeight*zoom);if(Math.abs(parseFloat(shell.style.width||'0')-expectedWidth)>2||Math.abs(parseFloat(shell.style.height||'0')-expectedHeight)>2)shellMismatch+=1;if(doc.querySelector('.zoom-label')?.textContent?.trim()!==`${Math.round(zoom*100)}%`)labelMismatch+=1}const ok=overlaps===0&&maxDrift<=1.5&&shellMismatch===0&&labelMismatch===0;diagnosticCheck(results,scenario.id,ok?'pass':'fail',`Геометрия обзора · ${profile.label}`,ok?'Карточки сохраняют координаты, не пересекаются, а оболочка масштабируется вместе с холстом.':`Пересечения: ${overlaps}; дрейф: ${maxDrift.toFixed(1)}px; ошибки оболочки: ${shellMismatch}; индикатора: ${labelMismatch}.`,{profileId:profile.id,viewport:profile.viewport,zoomLevels:profile.zoomLevels,overlaps,maxDrift,shellMismatch,labelMismatch,stage:{width:doc.querySelector('.stage')?.clientWidth||0,height:doc.querySelector('.stage')?.clientHeight||0}},null,ok?'low':'high')})}
1363
+ async function runMenuDiagnostic(results,scenario){closeWorkbenchMenus();let exclusivityFailures=0;const menus=[...document.querySelectorAll('.menu')];for(const menu of menus){menu.querySelector('summary')?.click();await nextPaint();if(document.querySelectorAll('.menu[open]').length!==1||!menu.open)exclusivityFailures+=1}document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await nextPaint();const escapeOpen=document.querySelectorAll('.menu[open]').length;menus[0]?.querySelector('summary')?.click();await nextPaint();$('.stage')?.dispatchEvent(new Event('pointerdown',{bubbles:true}));await nextPaint();const outsideOpen=document.querySelectorAll('.menu[open]').length;menus[0]?.querySelector('summary')?.click();await nextPaint();const diagnosticAction=document.createElement('button');diagnosticAction.type='button';diagnosticAction.hidden=true;diagnosticAction.dataset.diagnosticMenuAction='true';menus[0]?.querySelector('.menu-popover')?.append(diagnosticAction);diagnosticAction.click();diagnosticAction.remove();await nextPaint();const actionOpen=document.querySelectorAll('.menu[open]').length;const ok=exclusivityFailures===0&&escapeOpen===0&&outsideOpen===0&&actionOpen===0;diagnosticCheck(results,scenario.id,ok?'pass':'fail','Состояния верхнего меню',ok?'Открыто не больше одного меню; Escape, внешний клик и выбор команды закрывают меню.':`Сбои: эксклюзивность ${exclusivityFailures}, Escape ${escapeOpen}, внешний клик ${outsideOpen}, команда ${actionOpen}.`,{menus:menus.length,exclusivityFailures,escapeOpen,outsideOpen,actionOpen},null,ok?'low':'high');closeWorkbenchMenus()}
1364
+ function inspectLayoutRoot(root,screen){const view=root.ownerDocument.defaultView,overlaps=[],overflow=[],multiline=[],smallTargets=[],paddingVariance=[];const containers=[...root.querySelectorAll('.ui-container,.ui-card,.ui-list')];for(const parent of containers){const children=[...parent.children].filter(child=>child.classList.contains('ui-node')&&visibleElement(child)&&!['absolute','fixed'].includes(view.getComputedStyle(child).position));for(let i=0;i<children.length;i++)for(let j=i+1;j<children.length;j++){const area=rectOverlap(children[i].getBoundingClientRect(),children[j].getBoundingClientRect(),1);if(area>2)overlaps.push([children[i],children[j],area])}const buttons=children.filter(child=>child.matches('.ui-button'));if(buttons.length>1){const measures=buttons.map(button=>{const style=view.getComputedStyle(button);return {button,height:button.offsetHeight,pt:parseFloat(style.paddingTop)||0,pb:parseFloat(style.paddingBottom)||0}}),heights=measures.map(value=>value.height),vertical=measures.map(value=>value.pt+value.pb);if(Math.max(...heights)-Math.min(...heights)>1||Math.max(...vertical)-Math.min(...vertical)>1)paddingVariance.push(...buttons)}}for(const el of root.querySelectorAll('.ui-text,.ui-button')){if(!visibleElement(el))continue;const style=view.getComputedStyle(el),clipsX=el.scrollWidth>el.clientWidth+1&&!['auto','scroll'].includes(style.overflowX),clipsY=el.scrollHeight>el.clientHeight+1&&!['auto','scroll'].includes(style.overflowY);if(clipsX||clipsY)overflow.push(el);if(el.matches('.ui-button')&&el.offsetHeight<=44&&textLineCount(el)>1)multiline.push(el)}const screenVp=viewportFor(screen),minimum=screenVp.device==='phone'?44:32;for(const el of interactiveElements(root)){if(!visibleElement(el))continue;const rect=el.getBoundingClientRect();if(rect.width+1<minimum||rect.height+1<minimum)smallTargets.push(el)}return {overlaps,overflow,multiline,smallTargets,paddingVariance,minimum}}
1365
+ function inspectWorkbenchGeometry(doc=document){const controls=[...doc.querySelectorAll('.canvas-tools button,.menu-bar summary,.header-panel-button,.rail-button')].filter(visibleElement),multiline=controls.filter(control=>(control.textContent?.trim().length||0)>2&&textLineCount(control)>1),small=controls.filter(control=>{const rect=control.getBoundingClientRect();return rect.width<28||rect.height<28}),groups=[...doc.querySelectorAll('.mode-switch,.interaction-switch')],misaligned=[],occluded=[];for(const group of groups){const items=[...group.querySelectorAll(':scope>button')].filter(visibleElement);if(items.length<2)continue;const rects=items.map(item=>item.getBoundingClientRect()),tops=rects.map(rect=>rect.top),heights=rects.map(rect=>rect.height);if(Math.max(...tops)-Math.min(...tops)>1||Math.max(...heights)-Math.min(...heights)>1)misaligned.push(...items)}const canvasTools=doc.querySelector('.canvas-tools'),zoomControls=doc.querySelector('.floating-zoom'),panel=doc.querySelector('.inspector');if(visibleElement(canvasTools)&&visibleElement(zoomControls)&&rectOverlap(canvasTools.getBoundingClientRect(),zoomControls.getBoundingClientRect())>1)occluded.push(zoomControls);if(visibleElement(panel))for(const control of [canvasTools,zoomControls])if(visibleElement(control)&&rectOverlap(control.getBoundingClientRect(),panel.getBoundingClientRect())>1)occluded.push(control);return {multiline,small,misaligned,occluded}}
1366
+ async function runScreenRenderDiagnostic(results,scenario){for(const profile of diagnosticProfiles())await withDiagnosticProfile(profile,async({win,doc})=>{const api=win.__uiPreviewDiagnostics;for(const screen of screens){api.apply({view:'single',screen:screen.id,zoom:1,themeId:defaultThemeId,screenScenarioId:'default',nodeStates:{}});await nextPaintIn(win);const device=doc.querySelector(`.stage>.device[data-screen-id="${CSS.escape(screen.id)}"]`),root=device?.querySelector('.device-content'),expected=nodeIdsForScreen(screen),rendered=root?[...root.querySelectorAll('[data-node-id]')].map(item=>item.dataset.nodeId):[],missing=expected.filter(id=>!rendered.includes(id)),duplicates=[...new Set(rendered.filter((id,index)=>rendered.indexOf(id)!==index))],ok=Boolean(root)&&missing.length===0&&duplicates.length===0;diagnosticCheck(results,scenario.id,ok?'pass':'fail',`Перенос экрана · ${screen.name} · ${profile.label}`,ok?'Экран и все узлы IR присутствуют в HTML-проекции.':`Корневой контейнер: ${root?'есть':'нет'}; пропущено узлов: ${missing.length}; дубликатов: ${duplicates.length}.`,{profileId:profile.id,viewport:profile.viewport,expectedNodes:expected.length,renderedNodes:rendered.length,missing,duplicates},screen.id,ok?'low':'high')}})}
1367
+ async function runLayoutDiagnostic(results,scenario){
1368
+ for(const profile of diagnosticProfiles())await withDiagnosticProfile(profile,async({win,doc})=>{
1369
+ const api=win.__uiPreviewDiagnostics;
1370
+ for(const screen of screens){
1371
+ api.apply({view:'single',screen:screen.id,zoom:1});
1372
+ await nextPaintIn(win);
1373
+ const root=doc.querySelector('.stage>.device .device-content');
1374
+ if(!root){diagnosticCheck(results,scenario.id,'fail',`Макет · ${screen.name} · ${profile.label}`,'Корневой контейнер экрана не найден.',{scope:'product',profileId:profile.id,viewport:profile.viewport},screen.id,'high');continue}
1375
+ const report=inspectLayoutRoot(root,screen),fail=report.overlaps.length||report.overflow.length||report.multiline.length,warning=report.smallTargets.length||report.paddingVariance.length,result=fail?'fail':warning?'warning':'pass';
1376
+ diagnosticCheck(results,scenario.id,result,`Макет · ${screen.name} · ${profile.label}`,result==='pass'?'Пересечений, обрезки, случайных переносов и нестабильных целей не найдено.':`Пересечения ${report.overlaps.length}; обрезка ${report.overflow.length}; переносы в компактных кнопках ${report.multiline.length}; малые цели ${report.smallTargets.length}; разный padding/высота ${report.paddingVariance.length}.`,{scope:'product',profileId:profile.id,viewport:profile.viewport,overlaps:report.overlaps.map(pair=>pair.slice(0,2).map(nodeLabel)),overflow:report.overflow.map(nodeLabel),multiline:report.multiline.map(nodeLabel),smallTargets:report.smallTargets.map(nodeLabel),paddingVariance:report.paddingVariance.map(nodeLabel),minimumTarget:report.minimum},screen.id,fail?'high':'medium')
1377
+ }
1378
+ const chrome=inspectWorkbenchGeometry(doc),chromeFail=chrome.multiline.length||chrome.misaligned.length||chrome.occluded.length,result=chromeFail?'fail':chrome.small.length?'warning':'pass';
1379
+ diagnosticCheck(results,scenario.id,result,`Геометрия панели инструментов · ${profile.label}`,result==='pass'?'Контролы выровнены, подписи не переносятся, минимальные размеры соблюдены.':`Переносы ${chrome.multiline.length}; смещение уровней ${chrome.misaligned.length}; перекрытия панелью ${chrome.occluded.length}; цели меньше 28px ${chrome.small.length}.`,{scope:'workbench',profileId:profile.id,viewport:profile.viewport,multiline:chrome.multiline.map(nodeLabel),misaligned:chrome.misaligned.map(nodeLabel),occluded:chrome.occluded.map(nodeLabel),small:chrome.small.map(nodeLabel)},null,chromeFail?'high':'medium')
1380
+ })
1381
+ }
1382
+ async function runAccessibilityDiagnostic(results,scenario){for(const profile of diagnosticProfiles())await withDiagnosticProfile(profile,async({win,doc})=>{const api=win.__uiPreviewDiagnostics;for(const screen of screens)for(const theme of themes){api.apply({view:'single',screen:screen.id,themeId:theme.id,zoom:1});await nextPaintIn(win);const root=doc.querySelector('.stage>.device .device-content');if(!root)continue;const candidates=interactiveElements(root).filter(visibleElement),unnamed=[],symbolic=[];for(const el of candidates){const name=accessibleName(el);if(!name)unnamed.push(el);else if(!el.getAttribute('aria-label')&&!el.getAttribute('title')&&/^[\s‹›⌗+−×✓↺⇩{}◫⌖▶◇·]+$/u.test(name))symbolic.push(el)}const fail=unnamed.length||symbolic.length,themeLabel=themes.length>1?` · ${themeDisplayLabel(theme)}`:'';diagnosticCheck(results,scenario.id,fail?'fail':'pass',`Доступные имена · ${screen.name}${themeLabel} · ${profile.label}`,fail?`Без имени: ${compactList(unnamed)}; только неоднозначный символ: ${compactList(symbolic)}.`:'Интерактивные элементы имеют понятные доступные имена.',{profileId:profile.id,viewport:profile.viewport,themeId:theme.id,candidates:candidates.length,unnamed:unnamed.map(nodeLabel),symbolic:symbolic.map(nodeLabel)},screen.id,fail?'high':'low')}})}
1383
+ async function runStateMatrixDiagnostic(results,scenario,mode='review'){const cases=stateVariantCases();for(const profile of diagnosticProfiles())await withDiagnosticProfile(profile,async({win,doc})=>{const api=win.__uiPreviewDiagnostics;for(const item of cases){const screen=screens.find(value=>value.id===item.screenId);if(!screen)continue;api.apply({view:'single',screen:screen.id,zoom:1,themeId:item.themeId||defaultThemeId,screenScenarioId:item.kind==='screen'?item.scenarioId:'default',nodeStates:item.kind==='component'?{[item.nodeId]:item.stateName}:{}});await nextPaintIn(win);const root=doc.querySelector('.stage>.device .device-content'),caseLabel=item.kind==='screen'?`сценарий ${item.scenarioId}`:item.kind==='theme'?`тема ${themeDisplayLabel(themeById[item.themeId])}`:`${item.nodeId}:${item.stateName}`,metrics={profileId:profile.id,stateNodeId:item.nodeId||null,stateName:item.stateName,screenScenarioId:item.scenarioId||null,themeId:item.themeId||null};if(!root){diagnosticCheck(results,scenario.id,'fail',`Вариант · ${screen.name} · ${caseLabel}`,'Корневой контейнер не найден.',metrics,screen.id,'high');continue}if(mode==='projection'){diagnosticCheck(results,scenario.id,'pass',`Перенос варианта · ${screen.name} · ${caseLabel} · ${profile.label}`,'Объявленный вариант успешно отрисован в HTML.',{...metrics,viewport:profile.viewport},screen.id,'low');continue}const report=inspectLayoutRoot(root,screen),fail=report.overlaps.length||report.overflow.length||report.multiline.length,warning=report.smallTargets.length||report.paddingVariance.length,result=fail?'fail':warning?'warning':'pass';diagnosticCheck(results,scenario.id,result,`Вариант · ${screen.name} · ${caseLabel} · ${profile.label}`,result==='pass'?'Объявленный вариант сохраняет геометрию и доступность целей.':`Пересечения ${report.overlaps.length}; обрезка ${report.overflow.length}; переносы ${report.multiline.length}; малые цели ${report.smallTargets.length}; нестабильные размеры ${report.paddingVariance.length}.`,{...metrics,viewport:profile.viewport,overlaps:report.overlaps.map(pair=>pair.slice(0,2).map(nodeLabel)),overflow:report.overflow.map(nodeLabel),multiline:report.multiline.map(nodeLabel),smallTargets:report.smallTargets.map(nodeLabel),paddingVariance:report.paddingVariance.map(nodeLabel)},screen.id,fail?'high':warning?'medium':'low')}})}
1384
+ function navigationFlowKey(screen){return String(screen.navigationFlowId||screen.route||'review-default')}
1385
+ function runNavigationFlowDiagnostic(results,scenario,mode='review'){
1386
+ const targetScreens=reviewScopeScreens(),targetIds=new Set(targetScreens.map(screen=>screen.id)),allIds=new Set(screens.map(screen=>screen.id)),compiledNodes=Array.isArray(navigationGraph.nodes)?navigationGraph.nodes:[],compiledEdges=Array.isArray(navigationGraph.edges)?navigationGraph.edges:[],compiledFlows=Array.isArray(navigationGraph.flows)?navigationGraph.flows:[],hasCompiledGraph=compiledNodes.length>0||compiledEdges.length>0||compiledFlows.length>0,edges=[],invalid=[];
1387
+ for(const edge of compiledEdges){const normalized={from:String(edge.from||''),to:String(edge.to||''),kind:edge.kind||'navigate',evidence:edge.evidence||null,source:'navigationGraph'};if(!allIds.has(normalized.from)||!allIds.has(normalized.to))invalid.push(normalized);if(targetIds.has(normalized.from)&&targetIds.has(normalized.to))edges.push(normalized)}
1388
+ for(const target of Array.isArray(navigationGraph.unresolvedTargets)?navigationGraph.unresolvedTargets:[])invalid.push({kind:'unresolved-target',target,source:'navigationGraph'});
1389
+ for(const screen of targetScreens)for(const nodeId of nodeIdsForScreen(screen)){const action=nodes[nodeId]?.action;if(action?.type!=='navigate')continue;const edge={from:screen.id,to:String(action.target||''),nodeId,kind:'navigate',source:'node-action'};if(!allIds.has(edge.to))invalid.push(edge);else if(targetIds.has(edge.to)&&!edges.some(item=>item.from===edge.from&&item.to===edge.to))edges.push(edge)}
1390
+ const declaredEntries=new Set(Array.isArray(scenario.entryScreenIds)?scenario.entryScreenIds.map(String):[]),flows=[],unreachable=[];
1391
+ const flowDefinitions=hasCompiledGraph?compiledFlows.map(flow=>({flowId:String(flow.id||'review-default'),screenIds:(flow.screenIds||[]).map(String).filter(id=>targetIds.has(id)),entryScreenIds:(flow.entryScreenIds||[]).map(String)})).filter(flow=>flow.screenIds.length):[...new Set(targetScreens.map(navigationFlowKey))].map(flowId=>({flowId,screenIds:targetScreens.filter(screen=>navigationFlowKey(screen)===flowId).map(screen=>screen.id),entryScreenIds:[]}));
1392
+ const covered=new Set(flowDefinitions.flatMap(flow=>flow.screenIds));for(const screen of targetScreens)if(!covered.has(screen.id))flowDefinitions.push({flowId:navigationFlowKey(screen),screenIds:[screen.id],entryScreenIds:[]});
1393
+ let assessedFlows=0;
1394
+ for(const flow of flowDefinitions){const flowIds=new Set(flow.screenIds),internalEdges=edges.filter(edge=>flowIds.has(edge.from)&&flowIds.has(edge.to)),explicitEntries=new Set([...flow.entryScreenIds,...declaredEntries,...targetScreens.filter(screen=>screen.navigationEntry===true).map(screen=>screen.id)]),entries=flow.screenIds.filter(id=>explicitEntries.has(id));if(state.reviewScope==='current'&&!entries.length&&flow.screenIds.length===1)entries.push(flow.screenIds[0]);const reachabilityAssessed=mode==='review'&&entries.length>0;if(reachabilityAssessed)assessedFlows+=1;const visited=new Set(entries),queue=[...entries];while(queue.length){const from=queue.shift();for(const edge of internalEdges.filter(item=>item.from===from)){if(visited.has(edge.to))continue;visited.add(edge.to);queue.push(edge.to)}}const missing=reachabilityAssessed?flow.screenIds.filter(id=>!visited.has(id)):[];unreachable.push(...missing);flows.push({flowId:flow.flowId,screenIds:flow.screenIds,entryScreenIds:entries,edges:internalEdges,reachabilityAssessed,unreachable:missing})}
1395
+ const result=invalid.length?'fail':unreachable.length?'warning':'pass',message=invalid.length?`Некорректных целей перехода: ${invalid.length}.`:unreachable.length?`Недостижимых экранов из объявленных точек входа: ${unreachable.length}.`:mode==='projection'?'Все ссылки переходов разрешаются в собранные экраны.':assessedFlows?`Все экраны достижимы из объявленных точек входа в ${assessedFlows} flow.`:`Все цели переходов корректны. Достижимость не оценивалась: точки входа не объявлены.`;diagnosticCheck(results,scenario.id,result,mode==='projection'?'Перенос переходов':'Граф пользовательских переходов',message,{scope:'product',graphSource:hasCompiledGraph?'navigationGraph':'legacy',edges,invalid,unreachable,flows,assessedFlows,reviewScope:state.reviewScope},null,invalid.length?'high':unreachable.length?'medium':'low')
1396
+ }
1397
+ function parseRgb(value){const match=String(value||'').match(/rgba?\((\d+(?:\.\d+)?)[, ]+(\d+(?:\.\d+)?)[, ]+(\d+(?:\.\d+)?)(?:[, /]+([\d.]+))?/);return match?{r:Number(match[1]),g:Number(match[2]),b:Number(match[3]),a:match[4]==null?1:Number(match[4])}:null}
1398
+ function relativeLuminance(color){const channel=value=>{value/=255;return value<=.03928?value/12.92:Math.pow((value+.055)/1.055,2.4)};return .2126*channel(color.r)+.7152*channel(color.g)+.0722*channel(color.b)}
1399
+ function contrastRatio(a,b){const first=relativeLuminance(a),second=relativeLuminance(b);return (Math.max(first,second)+.05)/(Math.min(first,second)+.05)}
1400
+ function elementBackground(el){let current=el;while(current){const color=parseRgb(current.ownerDocument.defaultView.getComputedStyle(current).backgroundColor);if(color&&color.a>.01)return color;current=current.parentElement}return {r:255,g:255,b:255,a:1}}
1401
+ async function runContrastFocusDiagnostic(results,scenario){for(const profile of diagnosticProfiles())await withDiagnosticProfile(profile,async({win,doc})=>{const api=win.__uiPreviewDiagnostics;for(const screen of reviewScopeScreens())for(const theme of themes){api.apply({view:'single',screen:screen.id,themeId:theme.id,zoom:1,nodeStates:{}});await nextPaintIn(win);const root=doc.querySelector('.stage>.device .device-content');if(!root)continue;const lowContrast=[];for(const el of root.querySelectorAll('.ui-text,.ui-button,.ui-input')){if(!visibleElement(el)||!el.textContent?.trim()&&el.tagName!=='INPUT')continue;const style=win.getComputedStyle(el),foreground=parseRgb(style.color),background=elementBackground(el);if(!foreground)continue;const ratio=contrastRatio(foreground,background),large=parseFloat(style.fontSize)>=18||(parseFloat(style.fontSize)>=14&&Number(style.fontWeight)>=700),minimum=large?3:4.5;if(ratio+0.05<minimum)lowContrast.push(`${nodeLabel(el)}:${ratio.toFixed(2)}`)}const controls=interactiveElements(root).filter(visibleElement),untabbable=controls.filter(el=>el.tabIndex<0&&!el.disabled&&el.getAttribute('aria-disabled')!=='true'),fail=lowContrast.length||untabbable.length,themeLabel=themes.length>1?` · ${themeDisplayLabel(theme)}`:'';diagnosticCheck(results,scenario.id,fail?'fail':'pass',`Контраст и клавиатура · ${screen.name}${themeLabel} · ${profile.label}`,fail?`Недостаточный контраст: ${lowContrast.slice(0,5).join(', ')||'0'}; недоступны по Tab: ${compactList(untabbable)||'0'}.`:'Текст проходит вычисляемый контраст, интерактивные элементы доступны по Tab.',{profileId:profile.id,viewport:profile.viewport,themeId:theme.id,lowContrast,untabbable:untabbable.map(nodeLabel),controls:controls.length},screen.id,fail?'high':'low')}})}
1402
+ async function runDiagnostics(mode=new URL(location.href).searchParams.get('diagnostic-mode')||'projection'){
1403
+ if(state.diagnosticsRunning)return;
1404
+ const diagnosticMode=mode==='review'?'review':'projection',activeScenarios=(diagnosticsConfig.scenarios||[]).filter(scenario=>diagnosticMode==='review'||projectionDiagnosticKinds.has(scenario.kind));
1405
+ const snapshot={interaction:state.interaction,view:state.view,screen:state.screen,nodeStates:{...state.nodeStates},screenScenarioIds:{...state.screenScenarioIds},activeVersion:state.activeVersion,compareMode:state.compareMode,overlayPosition:state.overlayPosition,zoomMode:state.zoomMode,zoom:state.zoom,selectedNodeId:state.selectedNodeId,selectedScreenId:state.selectedScreenId,focusedFindingId:state.focusedFindingId,diagnosticTargetIds:[...state.diagnosticTargetIds],screenScrolls:JSON.parse(JSON.stringify(state.screenScrolls)),stageScroll:{...state.stageScroll}};
1406
+ const startedAt=new Date().toISOString(),results=[];
1407
+ state.activeVersion=baselineVersion;
1408
+ if(versionSelect)versionSelect.value=baselineVersion;
1409
+ state.diagnosticsRunning=true;
1410
+ state.diagnosticsProgress='Подготовка сценариев…';
1411
+ renderDiagnostics();renderCoverage();
1412
+ try{
1413
+ for(const scenario of activeScenarios){
1414
+ setDiagnosticsProgress(`Проверка: ${scenario.label}`);
1415
+ try{
1416
+ if(scenario.kind==='screen-render')await runScreenRenderDiagnostic(results,scenario);
1417
+ else if(scenario.kind==='zoom-reset')await runZoomResetDiagnostic(results,scenario);
1418
+ else if(scenario.kind==='overview-geometry')await runOverviewGeometryDiagnostic(results,scenario);
1419
+ else if(scenario.kind==='menu-exclusivity')await runMenuDiagnostic(results,scenario);
1420
+ else if(scenario.kind==='layout-integrity')await runLayoutDiagnostic(results,scenario);
1421
+ else if(scenario.kind==='accessibility-basics')await runAccessibilityDiagnostic(results,scenario);
1422
+ else if(scenario.kind==='state-matrix')await runStateMatrixDiagnostic(results,scenario,diagnosticMode);
1423
+ else if(scenario.kind==='navigation-flow')runNavigationFlowDiagnostic(results,scenario,diagnosticMode);
1424
+ else if(scenario.kind==='contrast-focus')await runContrastFocusDiagnostic(results,scenario);
1425
+ }catch(error){diagnosticCheck(results,scenario.id,'fail',scenario.label,`Сценарий завершился ошибкой: ${error?.message||error}.`,{stack:String(error?.stack||'')},null,'high')}
1426
+ }
1427
+ }finally{
1428
+ Object.assign(state,snapshot);
1429
+ if(versionSelect)versionSelect.value=state.activeVersion;
1430
+ state.diagnosticsRunning=false;
1431
+ state.diagnosticsProgress='';
1432
+ state.diagnostics={version:2,engineVersion:diagnosticsEngineVersion,status:'complete',mode:diagnosticMode,startedAt,completedAt:new Date().toISOString(),activeVersion:baselineVersion,reviewVersionId:baselineVersion,scope:state.reviewScope,screenIds:reviewScopeScreens().map(item=>item.id),profiles:diagnosticsConfig.profiles||[],scenarios:activeScenarios,summary:{pass:results.filter(item=>item.result==='pass').length,warning:results.filter(item=>item.result==='warning').length,fail:results.filter(item=>item.result==='fail').length,productWarning:results.filter(item=>item.result==='warning'&&item.metrics?.scope!=='workbench').length,productFail:results.filter(item=>item.result==='fail'&&item.metrics?.scope!=='workbench').length,toolWarning:results.filter(item=>item.result==='warning'&&item.metrics?.scope==='workbench').length,toolFail:results.filter(item=>item.result==='fail'&&item.metrics?.scope==='workbench').length},checks:results};
1433
+ writeLocation(false);renderView();persist();renderDiagnostics();renderCoverage();showToast(`Диагностика завершена: проблем продукта ${state.diagnostics.summary.productFail}, предупреждений ${state.diagnostics.summary.productWarning}`)
1434
+ }
1435
+ }
1436
+ function showAuditFindings(ids){state.findingFocus=ids.filter(id=>findings.some(item=>item.id===id));state.findingFilter='linked';persist();renderFindings();$('.automated-review')?.scrollIntoView({block:'start',behavior:'smooth'})}
1437
+ function acceptFindings(ids){for(const id of ids)if(findings.some(item=>item.id===id))state.findingDecisions[id]='accepted';persist();renderFindings();renderFixQueue();showToast(`В исправление добавлено: ${ids.length}`)}
1438
+ function clearFindingFocus(){state.findingFocus=[];state.findingFilter='all';persist();renderFindings()}
1439
+ function findingOrigin(item){if(item.runtimeDiagnosticId||item.runtimeDiagnosticIds?.length)return 'runtime';if(item.systemic)return 'systemic';return 'expert'}
1440
+ function renderFindings(){
1441
+ renderVersionStatus();
1442
+ const summary=$('.audit-summary'),list=$('.finding-list'),filters=$('.finding-filters'),context=$('.finding-context');
1443
+ document.querySelectorAll('.finding-total').forEach(total=>total.textContent=String(findings.length));
1444
+ if(!list)return;
1445
+ addFindingBadges();
1446
+ if(!findings.length){
1447
+ summary.innerHTML='<div class="problem-overview complete"><b>Проблем не найдено</b><span>Проверка завершена. Дополнительных действий не требуется.</span></div>';
1448
+ filters.hidden=true;context.hidden=true;list.innerHTML='';requestAnimationFrame(renderFindingMarkers);renderFixQueue();return;
1449
+ }
1450
+ filters.hidden=false;
1451
+ const counts={blocker:0,high:0,medium:0,low:0};for(const item of findings)counts[item.severity]=(counts[item.severity]||0)+1;
1452
+ const verifiedCount=findings.filter(findingResolved).length,addressedCount=findings.filter(item=>!findingResolved(item)&&findingAddressedInProposal(item)).length,unaddressedCount=findings.length-verifiedCount-addressedCount,gaps=Array.isArray(expertAudit.validationGaps)?expertAudit.validationGaps:[];
1453
+ const headline=verifiedCount===findings.length?'Все проблемы применены и проверены':addressedCount+verifiedCount===findings.length?'Новый макет учитывает все проблемы':`Нужно решить: ${unaddressedCount}`;
1454
+ const progress=verifiedCount===findings.length?`Проверено ${verifiedCount} из ${findings.length}`:`Найдено ${findings.length} · учтено в новом макете ${addressedCount} · применено и проверено ${verifiedCount}. ${addressedCount?'Новый макет ещё не применён к проекту.':''}`;
1455
+ summary.innerHTML=`<div class="problem-overview ${verifiedCount===findings.length?'complete':addressedCount+verifiedCount===findings.length?'addressed':''}"><b>${headline}</b><span>${progress}</span></div><details class="audit-details"><summary>Подробности проверки</summary><div class="audit-summary-copy">${esc(expertAudit.summary||'Ревью завершено.')}</div><div class="audit-counts"><span class="audit-count">Критичные ${counts.blocker}</span><span class="audit-count">Важные ${counts.high}</span><span class="audit-count">Обычные ${counts.medium}</span><span class="audit-count">Низкие ${counts.low}</span></div>${auditDepthMarkup()}${gaps.length?`<details class="audit-gaps"><summary>Нужно проверить вручную · ${gaps.length}</summary><ul>${gaps.map(item=>`<li>${esc(item)}</li>`).join('')}</ul></details>`:''}</details>`;
1456
+ summary.querySelectorAll('[data-audit-findings]').forEach(button=>button.addEventListener('click',()=>showAuditFindings(button.dataset.auditFindings.split(',').filter(Boolean))));
1457
+ summary.querySelectorAll('[data-accept-findings]').forEach(button=>button.addEventListener('click',()=>acceptFindings(button.dataset.acceptFindings.split(',').filter(Boolean))));
1458
+ const focused=state.findingFocus.filter(id=>findings.some(item=>item.id===id));context.hidden=!focused.length;
1459
+ if(focused.length)context.innerHTML=`<span>Показана ${focused.length} из ${findings.length} проблем</span><button type="button">Сбросить фильтр</button>`;
1460
+ context.querySelector('button')?.addEventListener('click',clearFindingFocus);
1461
+ document.querySelectorAll('.finding-filter').forEach(button=>button.setAttribute('aria-pressed',String(!focused.length&&button.dataset.findingFilter===state.findingFilter)));
1462
+ let visible;if(focused.length)visible=findings.filter(item=>focused.includes(item.id));else if(state.findingFilter==='accepted')visible=findings.filter(item=>state.findingDecisions[item.id]==='accepted');else if(state.findingFilter==='all')visible=[...findings];else visible=findings.filter(item=>item.severity===state.findingFilter);
1463
+ const severityLabels={blocker:'Критично',high:'Важно',medium:'Обычно',low:'Низко'};
1464
+ list.innerHTML=visible.length?orderedFindings(visible).map(item=>{
1465
+ const decision=state.findingDecisions[item.id]||'pending',evidence=Array.isArray(item.evidence)?item.evidence:[],proposalId=findingProposalId(item),verified=findingResolved(item),addressed=Boolean(proposalId),number=findingNumber(item.id),statusClass=verified?'resolved':decision==='accepted'?'selected':addressed?'addressed':'',statusText=verified?'Применено и проверено':decision==='accepted'?'Выбрано':addressed?'Учтено в новом макете':decision==='rejected'?'Не исправлять':decision==='deferred'?'Отложено':'Нужно решить';
1466
+ const openAction=proposalId?`<button type="button" class="finding-action primary" data-compare-finding="${esc(item.id)}">${iconMarkup('compare')}Сравнить</button>`:`<button type="button" class="finding-action" data-focus-finding="${esc(item.id)}">${iconMarkup('inspect')}Показать</button>`,selectAction=verified?'':`<button type="button" class="finding-action" data-finding-toggle="accepted" data-finding="${esc(item.id)}" aria-pressed="${decision==='accepted'}">${iconMarkup('check')}${decision==='accepted'?'Выбрано':'Выбрать'}</button>`,originalAction=proposalId?`<button type="button" class="finding-action" data-focus-finding="${esc(item.id)}">Показать исходный</button>`:'',moreAction=verified?(originalAction?`<details class="finding-more"><summary>Ещё</summary><div class="finding-more-actions">${originalAction}</div></details>`:''):`<details class="finding-more"><summary>Ещё</summary><div class="finding-more-actions">${originalAction}<button type="button" class="finding-action" data-finding-decision="deferred" data-finding="${esc(item.id)}" aria-pressed="${decision==='deferred'}">Позже</button><button type="button" class="finding-action" data-finding-decision="rejected" data-finding="${esc(item.id)}" aria-pressed="${decision==='rejected'}">Не исправлять</button></div></details>`;
1467
+ return `<article class="finding-card" data-finding-id="${esc(item.id)}" data-severity="${esc(item.severity)}" data-decision="${esc(decision)}"><div class="finding-card-header"><span class="finding-list-index">${number}</span><h3 class="finding-title">${esc(item.title)}</h3><span class="finding-severity">${esc(severityLabels[item.severity]||item.severity)}</span></div><span class="finding-card-status ${statusClass}">${esc(statusText)}</span><p class="finding-observation">${esc(item.observation)}</p><details class="finding-details"><summary>Почему это важно и как исправить</summary><p class="finding-impact"><b>Почему важно:</b> ${esc(item.impact)}</p><p class="finding-recommendation"><b>Что сделать:</b> ${esc(item.recommendation)}</p><div class="finding-meta">${esc(item.category)} · ${esc(item.confidence)} · ${esc(item.effort)}</div><details class="finding-evidence"><summary>Основания · ${evidence.length}</summary><ul>${evidence.map(value=>`<li><b>${esc(value.ref)}</b> — ${esc(value.note)}</li>`).join('')}</ul></details></details><div class="finding-actions">${openAction}${selectAction}${moreAction}</div></article>`;
1468
+ }).join(''):'<div class="panel-empty compact"><strong>Здесь пока пусто</strong><span>Измените фильтр или покажите все проблемы.</span></div>';
1469
+ list.querySelectorAll('[data-focus-finding]').forEach(button=>button.addEventListener('click',()=>focusFinding(button.dataset.focusFinding)));
1470
+ list.querySelectorAll('[data-compare-finding]').forEach(button=>button.addEventListener('click',()=>focusFinding(button.dataset.compareFinding,true)));
1471
+ list.querySelectorAll('[data-finding-toggle]').forEach(button=>button.addEventListener('click',()=>setFindingDecision(button.dataset.finding,state.findingDecisions[button.dataset.finding]==='accepted'?'pending':'accepted')));
1472
+ list.querySelectorAll('[data-finding-decision]').forEach(button=>button.addEventListener('click',()=>setFindingDecision(button.dataset.finding,button.dataset.findingDecision)));
1473
+ const selectable=findings.filter(item=>!findingResolved(item)),selected=selectedFindings();const accept=$('.accept-visible-findings'),clear=$('.clear-finding-decisions');if(accept){accept.disabled=Boolean(selectable.length)&&selectable.every(item=>state.findingDecisions[item.id]==='accepted');accept.textContent=accept.disabled?'Все выбрано':'Выбрать все'}if(clear)clear.disabled=!selected.length;
1474
+ requestAnimationFrame(renderFindingMarkers);renderFixQueue();
1475
+ }
1476
+ function populateFindingScreenFilter(){const select=$('.finding-screen-select');if(!select)return;const current=state.findingScreen,available=[...new Set(findings.map(item=>item.screenId).filter(Boolean))];select.innerHTML='<option value="all">Все экраны</option>'+available.map(id=>`<option value="${esc(id)}">${esc(screens.find(item=>item.id===id)?.name||id)}</option>`).join('');select.value=available.includes(current)?current:'all';if(select.value!==current)state.findingScreen='all'}
1477
+ function applyFindingExtraFilters(){populateFindingScreenFilter();const source=$('.finding-source-select'),screen=$('.finding-screen-select');if(source)source.value=state.findingSource;if(screen)screen.value=state.findingScreen;for(const card of document.querySelectorAll('.finding-card')){const id=card.querySelector('[data-finding]')?.dataset.finding||card.querySelector('[data-focus-finding]')?.dataset.focusFinding,item=findings.find(value=>value.id===id);if(!item)continue;const origin=findingOrigin(item),sourceMatch=state.findingSource==='all'||state.findingSource===origin||(state.findingSource==='systemic'&&item.systemic),screenMatch=state.findingScreen==='all'||item.screenId===state.findingScreen;card.hidden=!(sourceMatch&&screenMatch);card.dataset.origin=origin;const meta=card.querySelector('.finding-meta');if(meta&&!meta.querySelector('.finding-origin'))meta.insertAdjacentHTML('afterbegin',`<span class="finding-origin">${origin==='runtime'?'Автопроверка':origin==='systemic'?'Системная':'Эксперт'} · </span>`)}renderReviewNextAction();requestAnimationFrame(renderFindingMarkers)}
1478
+ function visibleFindingIds(){return [...document.querySelectorAll('.finding-card:not([hidden])')].map(card=>card.querySelector('[data-finding]')?.dataset.finding).filter(id=>id&&!findingResolved(findings.find(item=>item.id===id)))}
1479
+ function acceptVisibleFindings(){const ids=visibleFindingIds();for(const id of ids)state.findingDecisions[id]='accepted';state.agentHandoff=null;persist();renderFindings();showToast(`Выбрано проблем: ${selectedFindings().length}. Нажмите «Перейти к исправлению».`)}
1480
+ function fixAllInProject(){if(!findings.length){showToast('Сначала запустите ревью');return}if(!confirm(`Исправить в исходном проекте все найденные проблемы (${findings.length})? Полное повторное ревью автоматически запускаться не будет.`))return;for(const item of findings)state.findingDecisions[item.id]='accepted';state.agentHandoff=null;persist();renderFindings();setReviewSection('changes');prepareAgentTask('implement')}
1481
+ function clearVisibleFindingDecisions(){for(const id of visibleFindingIds())state.findingDecisions[id]='pending';state.agentHandoff=null;persist();renderFindings();showToast('Выбор видимых проблем снят')}
1482
+ function setAnnotationStatus(id,status){const item=state.annotations.find(a=>a.id===id);if(!item)return;item.status=status;item.updatedAt=new Date().toISOString();if(['resolved','rejected'].includes(status)&&state.openAnnotationId===id)state.openAnnotationId=null;persist();renderQueue();scheduleFindingMarkers()}
1483
+ function focusAnnotation(id){const item=state.annotations.find(a=>a.id===id);if(!item)return;setInspectorTab('comments');setScreen(item.screenId,'single');requestAnimationFrame(()=>{const el=document.querySelector(`[data-node-id="${CSS.escape(item.nodeId)}"]`);if(el){state.interaction='comment';syncInteractionButtons();selectNode(el);el.scrollIntoView({block:'center',inline:'center'})}})}
1484
+ function renderQueue(){const list=$('.annotation-list'),decision=$('.review-decision');document.querySelectorAll('.annotation-total').forEach(total=>total.textContent=String(state.annotations.length));if(!list)return;const decisions={pending:'Новый макет ещё не подтверждён',accepted:'Новый макет подтверждён · в проект не применён',rejected:'Новый макет требует доработки'};decision.textContent=decisions[state.versionDecision]||state.versionDecision;decision.className=`review-decision ${state.versionDecision}`;list.innerHTML=state.annotations.length?state.annotations.map(item=>`<article class="annotation-card" data-status="${esc(item.status||'new')}"><div class="annotation-meta"><span>${esc(item.priority||'medium')}</span><span>${esc(item.status||'new')}</span></div><p class="annotation-text" data-i18n-skip>${esc(item.text)}</p><button class="annotation-target" data-i18n-skip data-focus-annotation="${esc(item.id)}">${esc(item.screenId||'экран')} · ${esc(item.nodeId||'общее')}</button><div class="annotation-controls"><button class="annotation-status" data-annotation="${esc(item.id)}" data-status="in-progress">В работе</button><button class="annotation-status" data-annotation="${esc(item.id)}" data-status="accepted">Принять</button><button class="annotation-status" data-annotation="${esc(item.id)}" data-status="rejected">Отклонить</button><button class="annotation-status" data-annotation="${esc(item.id)}" data-status="resolved">Решено</button></div></article>`).join(''):'<div class="panel-empty compact"><strong>Комментариев нет</strong><span>Выберите инструмент комментариев и нажмите элемент на холсте.</span></div>';list.querySelectorAll('[data-annotation]').forEach(button=>button.addEventListener('click',()=>setAnnotationStatus(button.dataset.annotation,button.dataset.status)));list.querySelectorAll('[data-focus-annotation]').forEach(button=>button.addEventListener('click',()=>focusAnnotation(button.dataset.focusAnnotation)))}
1485
+ function feedbackPayload(){return {version:3,type:'ui-design-workbench-review-state',project:ir.project?.name||'Project',sessionId:review.sessionId||'default',targetRevision:reviewTargetRevision,revision:reviewRevision,workbenchSchema,diagnosticsEngineVersion,baselineVersion,activeVersion:state.activeVersion,versionDecision:state.versionDecision,findingDecisions:state.findingDecisions,runtimeFindings:state.runtimeFindings,annotations:state.annotations,diagnostics:state.diagnostics,reviewRuns:state.reviewRuns,importedReview:state.importedReview,viewContext:{view:state.view,screen:state.screen,reviewSection:state.reviewSection,reviewScope:state.reviewScope,findingFilter:state.findingFilter,findingSource:state.findingSource,findingScreen:state.findingScreen,compareMode:state.compareMode,compareBaseVersion:state.compareBaseVersion,compareTargetVersion:state.compareTargetVersion,activeThemeId:state.activeThemeId,screenScenarioIds:state.screenScenarioIds},exportedAt:new Date().toISOString()}}
1486
+ function restoreReviewStateData(raw){const payload=raw?.reviewFeedback||raw;if(!payload||typeof payload!=='object')throw new Error('Файл не содержит сохранённое ревью');const payloadRevision=payload.targetRevision||payload.revision;if(payloadRevision&&payloadRevision!==reviewTargetRevision)throw new Error('Ревью сохранено для другой версии макета');if(payload.project&&ir.project?.name&&payload.project!==ir.project.name)throw new Error('Ревью относится к другому проекту');if(payload.sessionId&&payload.sessionId!==(review.sessionId||'default'))throw new Error('Ревью относится к другой сессии');const diagnosticsCompatible=Number(payload.diagnosticsEngineVersion||payload.diagnostics?.engineVersion||0)===diagnosticsEngineVersion,portable=(Array.isArray(payload.runtimeFindings)?payload.runtimeFindings:[]).filter(item=>(diagnosticsCompatible||!isRuntimeFinding(item))&&item?.id&&(!item.screenId||screens.some(screen=>screen.id===item.screenId))&&(!item.nodeId||nodes[item.nodeId]));state.runtimeFindings=portable;const validIds=new Set(findings.map(item=>item.id));state.findingDecisions=Object.fromEntries(Object.entries(payload.findingDecisions||{}).filter(([id])=>validIds.has(id)));state.annotations=(Array.isArray(payload.annotations)?payload.annotations:[]).filter(item=>(!item.screenId||screens.some(screen=>screen.id===item.screenId))&&(!item.nodeId||nodes[item.nodeId])&&(!item.versionId||versionById[item.versionId]));state.diagnostics=diagnosticsCompatible&&payload.diagnostics?.mode==='review'?payload.diagnostics:null;state.reviewRuns=Array.isArray(payload.reviewRuns)?payload.reviewRuns:[];state.importedReview=payload.importedReview||null;state.versionDecision=payload.versionDecision||'pending';const context=payload.viewContext||{};if(payload.activeVersion&&versionById[payload.activeVersion])state.activeVersion=payload.activeVersion;if(['summary','problems','changes'].includes(context.reviewSection))state.reviewSection=context.reviewSection;if(['all','current'].includes(context.reviewScope))state.reviewScope=context.reviewScope;if(['overview','prototype','single','states','compare'].includes(context.view))state.view=context.view;if(context.screen&&screens.some(screen=>screen.id===context.screen))state.screen=context.screen;if(context.findingFilter)state.findingFilter=context.findingFilter;if(context.findingSource)state.findingSource=context.findingSource;if(context.findingScreen)state.findingScreen=context.findingScreen;if(context.compareMode)state.compareMode=context.compareMode;if(context.compareBaseVersion&&versionById[context.compareBaseVersion])state.compareBaseVersion=context.compareBaseVersion;if(context.compareTargetVersion&&versionById[context.compareTargetVersion])state.compareTargetVersion=context.compareTargetVersion;if(context.activeThemeId&&themeById[context.activeThemeId])state.activeThemeId=context.activeThemeId;if(context.screenScenarioIds&&typeof context.screenScenarioIds==='object')state.screenScenarioIds=context.screenScenarioIds;state.findingFocus=[];state.focusedFindingId=null;state.openFindingId=null;persist();renderView();showToast(diagnosticsCompatible?'Ревью восстановлено':'Ревью восстановлено; техническая проверка будет выполнена заново')}
1487
+ async function importFeedbackFile(file){if(!file)return;try{restoreReviewStateData(JSON.parse(await file.text()))}catch(error){showToast(`Не удалось восстановить ревью: ${error?.message||error}`)}}
1488
+ function fixRequestPayload(){const selected=selectedFindings(),context=agentTaskContext('proposal');return {version:3,type:'ui-design-workbench-fix-request',sessionId:review.sessionId||'default',revision:reviewRevision,project:ir.project?.name||'Project',baselineVersion,activeVersion:state.activeVersion,maxVariantCount:2,preferredVariantCount:1,contextFile:context.contextFile,patchFile:context.patchFile,acceptedFindingIds:selected.map(item=>item.id),existingProposalVersionIds:[...new Set(selected.map(item=>item.proposalVersionId).filter(Boolean))],findings:selected.map(item=>({id:item.id,title:item.title,category:item.category,severity:item.severity,screenId:item.screenId,nodeId:item.nodeId||null,observation:item.observation,impact:item.impact,recommendation:item.recommendation,effort:item.effort,proposalVersionId:item.proposalVersionId||null,decisionId:item.decisionId||null,runtimeDiagnosticId:item.runtimeDiagnosticId||null})),annotations:context.annotations,requestedAction:'Read bounded contextFile and write sparse proposal operations to patchFile. Preserve immutable baseline, create one restrained variant by default and a second only for a real UX tradeoff, then apply the patch and run targeted projection checks. Do not modify application source.',createdAt:new Date().toISOString()}}
1489
+ function sourceRequestPayload(){const context=agentTaskContext('implement');return {version:3,type:'ui-design-workbench-source-request',sessionId:review.sessionId||'default',revision:reviewRevision,project:ir.project?.name||'Project',projectRoot:context.projectRoot,contextFile:context.contextFile,approvedVersionId:state.versionDecision==='accepted'?state.activeVersion:null,acceptedFindingIds:context.acceptedFindingIds,findings:context.findings,sourceTargets:context.sourceTargets,sourceChangeAllowed:true,requestedAction:'Use bounded context and implement every accepted finding in real project source. Run incremental UI sync and targeted verification only; never repeat the full AI review automatically.',requiredChatReport:['number','findingId','screen','problem','implementedFix','changedFiles','verification','remainingReason'],createdAt:new Date().toISOString()}}
1490
+ function expertReviewRequestPayload(){const context=agentTaskContext('expert'),checks=state.diagnostics?.checks||[],failed=checks.filter(item=>item.status==='fail'||item.result==='fail'||item.issue);return {version:5,type:'ui-design-workbench-expert-review-request',sessionId:review.sessionId||'default',revision:reviewRevision,project:ir.project?.name||'Project',activeVersion:baselineVersion,reviewVersionId:baselineVersion,scope:state.reviewScope,screenIds:context.screenIds,contextFile:context.contextFile,patchFile:context.patchFile,maxVariantCount:2,preferredVariantCount:1,requestedAction:'Review only immutable Before in bounded screen batches. Use scoped context, never embed the full IR, group systemic causes, and return findings/proposals as a sparse ui-ir.patch.json without changing application source.',requiredChatReport:'Return a plain numbered list in chat with stable ID, severity, screen, problem, impact, recommendation, and status. Do not duplicate it as plain text in HTML.',expectedResult:{type:'ui-design-workbench-ir-patch',version:1,requestRevision:reviewRevision,sourceChangeAllowed:false},diagnosticsSummary:{engineVersion:diagnosticsEngineVersion,status:state.diagnostics?.status||null,checkCount:checks.length,failedCount:failed.length,failedIds:failed.slice(0,100).map(item=>item.id||item.checkId||item.code).filter(Boolean)},existingFindingIds:findings.map(item=>item.id),createdAt:new Date().toISOString()}}
1491
+ function renderCompareVersionOptions(){const afterId=activeAfterVersion();if(!versionById[state.compareBaseVersion])state.compareBaseVersion=baselineVersion;if(!versionById[state.compareTargetVersion])state.compareTargetVersion=afterId||baselineVersion;const fill=(select,value)=>{if(!select)return;select.innerHTML=versions.map(version=>`<option value="${esc(version.id)}" ${version.id===value?'selected':''}>${esc(versionDisplayLabel(version))}</option>`).join('')};fill($('.compare-base-select'),state.compareBaseVersion);fill($('.compare-target-select'),state.compareTargetVersion);syncCanvasLayerControls()}
1492
+ function addVersionToWorkbench(version){if(!version?.id||versionById[version.id])return false;const parent=version.parent||baselineVersion;if(parent&&!versionById[parent])throw new Error(`Неизвестная родительская версия: ${parent}`);const normalized={...version,parent,kind:version.kind||'proposal',status:version.status||'proposal',nodeOverrides:version.nodeOverrides||{},resolvedFindingIds:Array.isArray(version.resolvedFindingIds)?version.resolvedFindingIds:[]};versions.push(normalized);versionById[normalized.id]=normalized;const option=document.createElement('option');option.value=normalized.id;option.textContent=versionDisplayLabel(normalized);versionSelect.append(option);renderCompareVersionOptions();return true}
1493
+ function importExpertReviewData(payload){if(!payload||typeof payload!=='object')throw new Error('JSON не содержит объект результата');const incomingIr=payload.uiIr||payload.result?.uiIr||(payload.screens&&payload.nodes?payload:null),incomingReview=incomingIr?.review||payload.review||payload.result?.review||{},requestRevision=payload.requestRevision||payload.result?.requestRevision;if(requestRevision&&requestRevision!==reviewRevision)throw new Error('Результат создан для другой ревизии макета');const projectName=incomingIr?.project?.name||payload.project;if(projectName&&ir.project?.name&&projectName!==ir.project.name)throw new Error('Результат относится к другому проекту');if(incomingIr?.nodes)Object.assign(nodes,incomingIr.nodes);const incomingVersions=payload.versions||payload.result?.versions||incomingReview.versions||[],incomingFindings=payload.findings||payload.result?.findings||incomingReview.audit?.findings||[];let addedVersions=0,addedFindings=0,updatedFindings=0;for(const version of incomingVersions)if(addVersionToWorkbench(version))addedVersions+=1;for(const finding of incomingFindings){if(!finding?.id||!finding.title||!finding.screenId)continue;const existing=findings.find(item=>item.id===finding.id);if(existing){Object.assign(existing,finding,{origin:'expert-import'});updatedFindings+=1}else{state.runtimeFindings.push({...finding,origin:'expert-import'});addedFindings+=1}}const latestProposal=[...versions].reverse().find(item=>item.kind==='proposal'&&incomingVersions.some(value=>value.id===item.id));if(latestProposal){state.activeVersion=latestProposal.id;state.compareTargetVersion=latestProposal.id;versionSelect.value=latestProposal.id;state.versionDecision='pending';renderCompareVersionOptions()}state.importedReview={type:payload.type||'ui-design-workbench-expert-review-result',summary:payload.summary||payload.result?.summary||incomingReview.audit?.summary||'Результат импортирован',addedVersions,addedFindings,updatedFindings,importedAt:new Date().toISOString(),requestRevision:requestRevision||null};state.reviewRuns.push({id:`import-${Date.now()}`,createdAt:new Date().toISOString(),sessionName:state.reviewSessionName,scope:state.reviewScope,scopeLabel:'Импорт AI-агента',screenIds:reviewScopeScreens().map(item=>item.id),checks:state.diagnostics?.checks?.length||0,stateCases:(state.diagnostics?.checks||[]).filter(item=>item.scenarioId==='state-matrix').length,issues:incomingFindings.length,summary:{imported:true}});state.reviewRuns=state.reviewRuns.slice(-20);persist();renderView();renderReviewHistory();renderImportStatus();setReviewSection('changes');showToast(`Импортировано: вариантов ${addedVersions}, проблем ${addedFindings}, обновлено ${updatedFindings}`)}
1494
+ async function importExpertReviewFile(file){if(!file)return;const status=$('.import-review-status');try{if(status)status.textContent='Проверяем файл…';const payload=JSON.parse(await file.text());importExpertReviewData(payload)}catch(error){if(status)status.textContent=error?.message||String(error);showToast(`Импорт не выполнен: ${error?.message||error}`)}}
1495
+ function renderImportStatus(){const badge=$('.import-review-badge'),status=$('.import-review-status'),value=state.importedReview;if(badge)badge.textContent=value?'импортирован':'не импортирован';if(status)status.textContent=value?`${value.summary} · вариантов ${value.addedVersions}, новых проблем ${value.addedFindings}, обновлено ${value.updatedFindings}`:''}
1496
+ function downloadJson(filename,payload){const blob=new Blob([JSON.stringify(payload,null,2)+'\n'],{type:'application/json'}),url=URL.createObjectURL(blob),link=document.createElement('a');link.href=url;link.download=filename;link.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}
1497
+ function downloadFeedback(){const blob=new Blob([JSON.stringify(feedbackPayload(),null,2)+'\n'],{type:'application/json'}),url=URL.createObjectURL(blob),link=document.createElement('a');link.href=url;link.download='ui-review-state.json';link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);showToast('Ревью сохранено в файл')}
1498
+ function syncInteractionButtons(){document.querySelectorAll('[data-interaction]').forEach(item=>item.setAttribute('aria-pressed',String(item.dataset.interaction===state.interaction)))}
1499
+ function syncRailBadges(){document.querySelectorAll('.rail-badge').forEach(badge=>badge.hidden=badge.textContent.trim()==='0')}
1500
+ const railBadgeObserver=new MutationObserver(syncRailBadges);document.querySelectorAll('.rail-badge').forEach(badge=>railBadgeObserver.observe(badge,{childList:true,characterData:true,subtree:true}));syncRailBadges();
1501
+ bindCanvasPan($('.stage'));const findingGeometryObserver=new ResizeObserver(()=>scheduleFindingMarkers());findingGeometryObserver.observe($('.stage'));
1502
+ window.__uiPreviewDiagnostics={apply(options={}){state.activeVersion=options.activeVersion&&versionById[options.activeVersion]?options.activeVersion:baselineVersion;if(versionSelect)versionSelect.value=state.activeVersion;if(options.view)state.view=options.view;if(options.screen&&screens.some(screen=>screen.id===options.screen))state.screen=options.screen;if(options.themeId&&themeById[options.themeId])state.activeThemeId=options.themeId;if(options.screenScenarioId!==undefined&&state.screen)state.screenScenarioIds[state.screen]=options.screenScenarioId;if(options.nodeStates&&typeof options.nodeStates==='object')state.nodeStates={...options.nodeStates};if(options.zoom!==undefined){state.zoomMode='manual';state.zoom=clampZoom(Number(options.zoom)||1)}renderView()},report(){return state.diagnostics},agentHandoff(kind='expert'){return agentTaskDescriptor(kind)},codexHandoff(kind='expert'){return agentTaskDescriptor(kind)}};
1503
+ workbenchLocaleObserver.observe($('.review-app'),{subtree:true,childList:true,characterData:true,attributes:true,attributeFilter:['aria-label','title','placeholder']});renderScreenTree();bindWorkbenchMenus();document.querySelectorAll('[data-workbench-locale]').forEach(button=>button.addEventListener('click',()=>setWorkbenchLocale(button.dataset.workbenchLocale)));$('.screen-search')?.addEventListener('input',event=>filterScreenTree(event.currentTarget.value));document.querySelectorAll('[data-inspector-tab]').forEach(button=>button.addEventListener('click',()=>activateRailTool(button.dataset.inspectorTab)));document.querySelectorAll('[data-interaction]').forEach(button=>button.addEventListener('click',()=>{state.interaction=button.dataset.interaction;if(state.interaction==='inspect')setInspectorTab('inspect');if(state.interaction==='comment')setInspectorTab('comments');syncInteractionButtons();renderView()}));document.querySelectorAll('[data-view]').forEach(button=>button.addEventListener('click',()=>{closeWorkbenchMenus();setView(button.dataset.view)}));document.querySelectorAll('[data-canvas-layer]').forEach(button=>button.addEventListener('click',()=>toggleCanvasLayer(button.dataset.canvasLayer)));document.querySelectorAll('[data-version-visibility]').forEach(button=>button.addEventListener('click',()=>setVersionVisibility(button.dataset.versionVisibility)));document.querySelectorAll('[data-compare-mode]').forEach(button=>button.addEventListener('click',()=>{if(button.disabled)return;state.compareMode=button.dataset.compareMode;persist();renderView()}));document.querySelectorAll('[data-finding-filter]').forEach(button=>button.addEventListener('click',()=>{state.findingFocus=[];state.findingFilter=button.dataset.findingFilter;persist();renderFindings()}));document.querySelectorAll('.sidebar-toggle').forEach(button=>button.addEventListener('click',()=>{const opening=!state.sidebarOpen;state.sidebarOpen=opening;if(opening&&innerWidth<=980)state.inspectorOpen=false;persist();applyPanelState();requestAnimationFrame(()=>renderView())}));document.querySelectorAll('.inspector-toggle').forEach(button=>button.addEventListener('click',()=>{const opening=!state.inspectorOpen;state.inspectorOpen=opening;if(opening&&innerWidth<=980)state.sidebarOpen=false;persist();applyPanelState();requestAnimationFrame(()=>renderView())}));document.querySelectorAll('.panel-resizer').forEach(bindResizer);
1504
+ document.querySelectorAll('[data-zoom=out]').forEach(button=>button.addEventListener('click',()=>setZoom(state.computedZoom-.1)));document.querySelectorAll('[data-zoom=in]').forEach(button=>button.addEventListener('click',()=>setZoom(state.computedZoom+.1)));document.querySelectorAll('[data-zoom=reset]').forEach(button=>button.addEventListener('click',()=>setZoom(1)));document.querySelectorAll('[data-zoom=fit]').forEach(button=>button.addEventListener('click',()=>{state.zoomMode='fit';persist();closeWorkbenchMenus();renderView()}));
1505
+ const versionSelect=$('.version-select');for(const version of versions){const option=document.createElement('option');option.value=version.id;option.textContent=versionDisplayLabel(version);option.selected=version.id===state.activeVersion;versionSelect.append(option)}versionSelect.addEventListener('change',()=>{state.activeVersion=versionSelect.value;state.compareTargetVersion=state.activeVersion;persist();renderCompareVersionOptions();renderView()});renderCompareVersionOptions();$('.compare-base-select')?.addEventListener('change',event=>{state.compareBaseVersion=event.currentTarget.value;persist();renderView()});$('.compare-target-select')?.addEventListener('change',event=>{state.compareTargetVersion=event.currentTarget.value;state.activeVersion=event.currentTarget.value;versionSelect.value=state.activeVersion;persist();renderView()});
1506
+ document.querySelectorAll('[data-version-decision=accepted]').forEach(button=>button.addEventListener('click',()=>{state.versionDecision='accepted';persist();renderQueue();renderFixQueue();showToast('Версия отмечена как принята')}));document.querySelectorAll('[data-version-decision=rejected]').forEach(button=>button.addEventListener('click',()=>{state.versionDecision='rejected';persist();renderQueue();renderFixQueue();showToast('Версия отправлена на доработку')}));document.querySelectorAll('.export-feedback').forEach(button=>button.addEventListener('click',downloadFeedback));document.querySelectorAll('.import-feedback').forEach(button=>button.addEventListener('click',()=>$('.import-feedback-file')?.click()));$('.import-feedback-file')?.addEventListener('change',event=>{const file=event.currentTarget.files?.[0];importFeedbackFile(file);event.currentTarget.value=''});document.querySelectorAll('.copy-feedback').forEach(button=>button.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(JSON.stringify(feedbackPayload(),null,2));showToast('Резервная копия ревью скопирована')}catch(_){showToast('Не удалось скопировать JSON — используйте сохранение в файл')}}));
1507
+ $('.run-diagnostics')?.addEventListener('click',runAutomatedReview);$('.clear-diagnostics')?.addEventListener('click',()=>{clearRuntimeReview();persist();renderFindings();renderDiagnostics();renderReviewLauncher();renderCoverage();renderReviewNextAction();showToast('Технические результаты и связанные с ними проблемы очищены')});
1508
+ $('.run-review')?.addEventListener('click',runAutomatedReview);document.querySelectorAll('.open-agent-review').forEach(button=>button.addEventListener('click',()=>{if(state.diagnostics)prepareAgentTask('expert')}));document.querySelectorAll('.export-review-request').forEach(button=>button.addEventListener('click',()=>{if(!state.diagnostics)return;downloadJson('ui-expert-review-request.json',expertReviewRequestPayload());showToast('JSON-задание для AI-ревью скачано')}));
1509
+ $('.export-fix-request')?.addEventListener('click',()=>{if(!selectedFindings().length)return;downloadJson('ui-fix-request.json',fixRequestPayload());showToast('Задание для AI-агента подготовлено')});$('.copy-fix-request')?.addEventListener('click',async()=>{if(!selectedFindings().length)return;try{await navigator.clipboard.writeText(JSON.stringify(fixRequestPayload(),null,2));showToast('Задание для AI-агента скопировано')}catch(_){showToast('Не удалось скопировать — используйте загрузку файла')}});
1510
+ $('.prepare-source-request')?.addEventListener('click',()=>{if(versionById[state.activeVersion]?.kind!=='proposal'||state.versionDecision!=='accepted')return;state.sourcePreparedVersionId=state.activeVersion;persist();renderFixQueue();downloadJson('ui-source-change-request.json',sourceRequestPayload());showToast('Подготовлен запрос на внедрение; изменение кода ещё не разрешено')});
1511
+ $('.migrate-review-state')?.addEventListener('click',migrateReviewState);$('.discard-review-state')?.addEventListener('click',discardReviewState);
1512
+ $('.open-fix-queue')?.addEventListener('click',()=>{setInspectorTab('review');setReviewSection('changes');const panel=$('.fix-queue');if(panel)panel.open=true;closeWorkbenchMenus()});
1513
+ $('.open-diagnostics')?.addEventListener('click',()=>{setInspectorTab('review');setReviewSection('summary');const panel=$('.runtime-diagnostics');if(panel)panel.open=true;closeWorkbenchMenus()});
1514
+ document.querySelectorAll('[data-review-section]').forEach(button=>button.addEventListener('click',()=>setReviewSection(button.dataset.reviewSection)));$('.review-scope-select')?.addEventListener('change',event=>{state.reviewScope=event.currentTarget.value;clearRuntimeReview();state.agentHandoff=null;persist();renderFindings();renderReviewLauncher();renderCoverage();renderReviewNextAction()});$('.finding-source-select')?.addEventListener('change',event=>{state.findingSource=event.currentTarget.value;persist();applyFindingExtraFilters()});$('.finding-screen-select')?.addEventListener('change',event=>{state.findingScreen=event.currentTarget.value;persist();applyFindingExtraFilters()});$('.accept-visible-findings')?.addEventListener('click',acceptVisibleFindings);$('.fix-all-source')?.addEventListener('click',fixAllInProject);$('.clear-finding-decisions')?.addEventListener('click',clearVisibleFindingDecisions);$('.history-clear')?.addEventListener('click',()=>{state.reviewRuns=[];persist();renderReviewHistory();showToast('История запусков очищена')});$('.review-next-action')?.addEventListener('click',handleReviewNextAction);$('.review-reject-action')?.addEventListener('click',rejectActiveVersion);$('.copy-agent-task')?.addEventListener('click',copyAgentTask);$('.refresh-preview')?.addEventListener('click',refreshPreview);$('.import-review-result')?.addEventListener('click',()=>$('.import-review-file')?.click());$('.import-review-file')?.addEventListener('change',event=>{const file=event.currentTarget.files?.[0];importExpertReviewFile(file);event.currentTarget.value=''});const findingListObserver=new MutationObserver(applyFindingExtraFilters);if($('.finding-list'))findingListObserver.observe($('.finding-list'),{childList:true});
1515
+ $('.review-session-name')?.addEventListener('change',event=>{state.reviewSessionName=event.currentTarget.value.trim()||review.sessionId||'Ревью';persist();renderReviewHistory()});
1516
+ renderReviewLauncher();renderReviewSections();renderCoverage();renderReviewHistory();renderImportStatus();renderAgentHandoff();
1517
+ addEventListener('popstate',()=>{const requested=normalizeWorkbenchLocale(new URL(location.href).searchParams.get('lang'));if(requested&&requested!==workbenchLocale)setWorkbenchLocale(requested,{remember:false,updateLocation:false});state.screen=currentFromLocation();state.view=viewFromLocation();state.activeThemeId=themeFromLocation();state.variantAxis=axisFromLocation()||state.variantAxis;renderView()});addEventListener('resize',()=>{if(innerWidth<=980&&state.sidebarOpen&&state.inspectorOpen){state.sidebarOpen=false;persist();applyPanelState();renderView();return}requestAnimationFrame(renderFindingMarkers)});addEventListener('beforeunload',persist);if(innerWidth<=980&&state.sidebarOpen&&state.inspectorOpen)state.sidebarOpen=false;state.screen=currentFromLocation();state.view=viewFromLocation();const requestedTheme=new URL(location.href).searchParams.get('theme'),requestedAxis=axisFromLocation();if(requestedTheme&&themeById[requestedTheme])state.activeThemeId=requestedTheme;if(requestedAxis)state.variantAxis=requestedAxis;writeLocation(false);syncInteractionButtons();renderView();localizeWorkbench();if(autoMigratedReviewState){persist();setTimeout(()=>showToast('Состояние ревью восстановлено после обновления интерфейса'),0)}if(new URL(location.href).searchParams.get('diagnostics')==='run')setTimeout(runDiagnostics,0);
1518
+ """
1519
+
1520
+
1521
+ ICON_SPRITE = """
1522
+ <svg class="icon-sprite" aria-hidden="true" focusable="false">
1523
+ <symbol id="icon-logo" viewBox="0 0 16 16"><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><path d="M11.5 9v5M9 11.5h5"/></symbol>
1524
+ <symbol id="icon-screens" viewBox="0 0 16 16"><rect x="2" y="2.5" width="12" height="9" rx="1.5"/><path d="M5 14h6M8 11.5V14"/></symbol>
1525
+ <symbol id="icon-grid" viewBox="0 0 16 16"><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></symbol>
1526
+ <symbol id="icon-play" viewBox="0 0 16 16"><path d="m5 3 8 5-8 5V3Z"/></symbol>
1527
+ <symbol id="icon-frame" viewBox="0 0 16 16"><rect x="2.5" y="2.5" width="11" height="11" rx="1.5"/><path d="M5 2.5v11M11 2.5v11"/></symbol>
1528
+ <symbol id="icon-compare" viewBox="0 0 16 16"><rect x="2" y="2.5" width="5" height="11" rx="1"/><rect x="9" y="2.5" width="5" height="11" rx="1"/></symbol>
1529
+ <symbol id="icon-pointer" viewBox="0 0 16 16"><path d="m3 2 8.5 7H7.8L6 13.5 3 2Z"/></symbol>
1530
+ <symbol id="icon-inspect" viewBox="0 0 16 16"><circle cx="8" cy="8" r="3"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2"/></symbol>
1531
+ <symbol id="icon-comment" viewBox="0 0 16 16"><path d="M3 2.5h10a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7l-3.5 2v-2H3a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z"/></symbol>
1532
+ <symbol id="icon-review" viewBox="0 0 16 16"><path d="M3 2.5h10v11H3z"/><path d="m5 6 1.2 1.2L8.5 5M5 10h6"/></symbol>
1533
+ <symbol id="icon-search" viewBox="0 0 16 16"><circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3 3"/></symbol>
1534
+ <symbol id="icon-chevron-left" viewBox="0 0 16 16"><path d="m10 3-5 5 5 5"/></symbol>
1535
+ <symbol id="icon-chevron-right" viewBox="0 0 16 16"><path d="m6 3 5 5-5 5"/></symbol>
1536
+ <symbol id="icon-panel-left" viewBox="0 0 16 16"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M6 2.5v11"/></symbol>
1537
+ <symbol id="icon-panel-right" viewBox="0 0 16 16"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M10 2.5v11"/></symbol>
1538
+ <symbol id="icon-minus" viewBox="0 0 16 16"><path d="M3 8h10"/></symbol>
1539
+ <symbol id="icon-plus" viewBox="0 0 16 16"><path d="M3 8h10M8 3v10"/></symbol>
1540
+ <symbol id="icon-fit" viewBox="0 0 16 16"><path d="M6 2H2v4M10 2h4v4M6 14H2v-4M10 14h4v-4"/></symbol>
1541
+ <symbol id="icon-more" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1"/><circle cx="8" cy="8" r="1"/><circle cx="13" cy="8" r="1"/></symbol>
1542
+ <symbol id="icon-activity" viewBox="0 0 16 16"><path d="M2 8h3l1.5-4 3 8L11 8h3"/></symbol>
1543
+ <symbol id="icon-check" viewBox="0 0 16 16"><path d="m3 8 3 3 7-7"/></symbol>
1544
+ <symbol id="icon-close" viewBox="0 0 16 16"><path d="m4 4 8 8M12 4l-8 8"/></symbol>
1545
+ <symbol id="icon-download" viewBox="0 0 16 16"><path d="M8 2v8M5 7l3 3 3-3M3 13h10"/></symbol>
1546
+ <symbol id="icon-menu" viewBox="0 0 16 16"><path d="M3 4h10M3 8h10M3 12h10"/></symbol>
1547
+ <symbol id="icon-globe" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"/><path d="M2 8h12M8 2c2 2 2 10 0 12M8 2C6 4 6 12 8 14"/></symbol>
1548
+ <symbol id="icon-copy" viewBox="0 0 16 16"><rect x="5" y="5" width="8" height="8" rx="1"/><path d="M3 10H2V3a1 1 0 0 1 1-1h7v1"/></symbol>
1549
+ <symbol id="icon-external" viewBox="0 0 16 16"><path d="M9 2h5v5M14 2 8 8"/><path d="M7 3H3a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V9"/></symbol>
1550
+ <symbol id="icon-back" viewBox="0 0 16 16"><path d="M6.5 3 2 7.5 6.5 12M2.5 7.5H10a4 4 0 0 1 4 4"/></symbol>
1551
+ </svg>
1552
+ """
1553
+
1554
+
1555
+ def icon(name: str, class_name: str = "icon") -> str:
1556
+ return f'<svg class="{class_name}" aria-hidden="true"><use href="#icon-{name}"></use></svg>'
1557
+
1558
+
1559
+ def render_html(
1560
+ ir: dict[str, Any],
1561
+ artifact_dir: Path | None = None,
1562
+ handoff_provider: str = "generic",
1563
+ ) -> str:
1564
+ project = ir.get("project", {})
1565
+ screens = ir.get("screens", [])
1566
+ warnings = list(ir.get("warnings", []))
1567
+ audit = ir.get("fidelityAudit", {})
1568
+ if audit.get("nativeEvidenceRequired") and not audit.get("nativeExecutionStarted"):
1569
+ warnings.append("HTML показывает структурную проекцию. Нативный Android/iOS-рендер не запускался, визуальная точность не подтверждена.")
1570
+ font_faces: list[str] = []
1571
+ for font in ir.get("fonts", []):
1572
+ if not font.get("resolvedAsset") or not font.get("family"):
1573
+ continue
1574
+ family = str(font["family"]).replace("\\", "\\\\").replace("'", "\\'")
1575
+ weight = html.escape(str(font.get("weight", "400")), quote=True)
1576
+ font_style = html.escape(str(font.get("style", "normal")), quote=True)
1577
+ font_faces.append(
1578
+ f"@font-face{{font-family:'{family}';src:url('{font['resolvedAsset']}');"
1579
+ f"font-weight:{weight};font-style:{font_style};font-display:block}}"
1580
+ )
1581
+ font_css = "".join(font_faces)
1582
+ encoded_ir = json.dumps(ir, ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c")
1583
+ encoded_context = json.dumps(
1584
+ {
1585
+ "artifactDir": str(artifact_dir.resolve()) if artifact_dir else "",
1586
+ "handoffProvider": handoff_provider,
1587
+ "diagnosticsEngineVersion": DIAGNOSTICS_ENGINE_VERSION,
1588
+ "interactionContract": browser_interaction_contract(),
1589
+ },
1590
+ ensure_ascii=False,
1591
+ separators=(",", ":"),
1592
+ ).replace("<", "\\u003c")
1593
+ screen_buttons = "".join(
1594
+ f'<button type="button" class="screen-link" data-screen="{html.escape(str(screen["id"]))}" aria-current="false">{html.escape(str(screen["name"]))}</button>'
1595
+ for screen in screens
1596
+ )
1597
+ warning_markup = ""
1598
+ if warnings:
1599
+ items = "".join(f"<li>{html.escape(str(item))}</li>" for item in warnings)
1600
+ warning_markup = f'<details class="warning-panel"><summary>Предупреждения <span data-i18n-skip>({len(warnings)})</span></summary><ul data-i18n-skip>{items}</ul></details>'
1601
+ origin_label = "Связь с исходниками" if audit.get("designMode") == "reconstruct" else "Основания решений"
1602
+ origin_value = audit.get("sourceCoverage", 0) if audit.get("designMode") == "reconstruct" else audit.get("evidenceCoverage", 0)
1603
+ standards_markup = "" if audit.get("designMode") == "reconstruct" else (
1604
+ f'<span>Стандарты:</span> {audit.get("standardCoverage", 0):.0%} · <span>Семантика:</span> {audit.get("semanticCoverage", 0):.0%} · '
1605
+ f'<span>Размер целей:</span> {audit.get("targetCoverage", 0):.0%} · <span>Контраст:</span> {audit.get("contrastCoverage", 0):.0%} · '
1606
+ f'<span>Состояния:</span> {audit.get("stateCoverage", 0):.0%}<br>'
1607
+ )
1608
+ native_markup = ""
1609
+ if audit.get("nativeEvidenceRequired"):
1610
+ native_status = "проверен" if audit.get("nativeExecutionStarted") and audit.get("nativeEvidenceCoverage", 0) > 0 else "не проверен"
1611
+ native_markup = (
1612
+ f'<span>Визуальная точность:</span> <span data-i18n-skip>{html.escape(str(audit.get("visualFidelityTier", "structural")))}</span> · '
1613
+ f'<span>Нативный снимок:</span> {native_status}<br>'
1614
+ )
1615
+ audit_markup = (
1616
+ f'<details class="audit-panel"><summary>Проверка покрытия</summary><p class="audit"><span>Режим:</span> <span data-i18n-skip>{html.escape(str(audit.get("designMode", "reconstruct")))}</span><br>'
1617
+ f'<span>{origin_label}:</span> {origin_value:.0%} · '
1618
+ f'<span>Оформление:</span> {audit.get("appearanceCoverage", 0):.0%} · '
1619
+ f'<span>Высокая уверенность:</span> {audit.get("highConfidenceCoverage", 0):.0%} · '
1620
+ f'<span>Компоненты:</span> {audit.get("componentCoverage", 1):.0%} ({audit.get("catalogComponents", 0)})<br>'
1621
+ f'{native_markup}'
1622
+ f'{standards_markup}'
1623
+ f'<span>Экраны:</span> {audit.get("screenCoverage", 0):.0%} · <span>Маршруты:</span> {audit.get("routeCoverage", 0):.0%} · '
1624
+ f'<span>Цели меню:</span> {audit.get("navigationCoverage", 1):.0%} · '
1625
+ f'<span>Переходы:</span> {audit.get("navigationActions", 0)}</p></details>'
1626
+ )
1627
+ title = f'{project.get("name", "Project")} · UI Design Workbench'
1628
+ return f"""<!doctype html>
1629
+ <html lang="ru">
1630
+ <head>
1631
+ <meta charset="utf-8">
1632
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1633
+ <title>{html.escape(title)}</title>
1634
+ <style>{font_css}{CSS}{VIEW_CSS}{REVIEW_CSS}</style>
1635
+ </head>
1636
+ <body>
1637
+ {ICON_SPRITE}
1638
+ <div class="review-app">
1639
+ <nav class="workbench-rail" aria-label="Разделы рабочего пространства">
1640
+ <details class="menu rail-menu file-menu"><summary class="rail-button" aria-label="Файл" title="Файл">{icon("menu")}</summary><div class="menu-popover">
1641
+ <button type="button" class="export-feedback">Сохранить ревью</button><button type="button" class="import-feedback">Восстановить ревью</button><button type="button" class="copy-feedback">Копировать резервную копию</button><input type="file" class="import-feedback-file" accept="application/json,.json" hidden>
1642
+ </div></details>
1643
+ <div class="rail-divider"></div>
1644
+ <button type="button" class="rail-button sidebar-toggle" data-rail-panel="screens" aria-pressed="true" aria-label="Экраны" title="Экраны">{icon("screens")}</button>
1645
+ <div class="rail-divider"></div>
1646
+ <button type="button" class="rail-button" data-inspector-tab="inspect" aria-pressed="false" aria-label="Свойства выбранного элемента" title="Свойства">{icon("inspect")}</button>
1647
+ <button type="button" class="rail-button" data-inspector-tab="review" aria-pressed="true" aria-label="Ревью и исправления" title="Ревью">{icon("review")}<span class="rail-badge finding-total">0</span></button>
1648
+ <button type="button" class="rail-button" data-inspector-tab="comments" aria-pressed="false" aria-label="Комментарии" title="Комментарии">{icon("comment")}<span class="rail-badge annotation-total">0</span></button>
1649
+ <div class="rail-divider"></div>
1650
+ <details class="menu rail-menu language-menu"><summary class="rail-button" aria-label="Язык интерфейса" title="Язык интерфейса">{icon("globe")}<span class="rail-locale-badge locale-current">RU</span></summary><div class="menu-popover" role="radiogroup" aria-label="Язык интерфейса"><button type="button" class="locale-option" data-workbench-locale="ru" role="radio" aria-checked="true">Русский</button><button type="button" class="locale-option" data-workbench-locale="en" role="radio" aria-checked="false">English</button></div></details>
1651
+ <div class="rail-spacer"></div>
1652
+ <button type="button" class="rail-button inspector-toggle" aria-pressed="true" aria-label="Скрыть или показать правую панель" title="Правая панель">{icon("panel-right")}</button>
1653
+ </nav>
1654
+ <aside class="sidebar">
1655
+ <header class="side-panel-header"><div><span class="panel-kicker">Навигация</span><h1 class="side-panel-title">Экраны</h1></div><button type="button" class="panel-close sidebar-toggle" aria-label="Скрыть панель экранов" title="Скрыть панель">{icon("chevron-left")}</button></header>
1656
+ <div class="sidebar-project" title="{html.escape(str(project.get('name', 'Project')))}">{html.escape(str(project.get("name", "Project")))}</div>
1657
+ <label class="tree-search">{icon("search")}<input type="search" class="screen-search" placeholder="Найти экран" aria-label="Найти экран"></label>
1658
+ <nav class="screen-list" aria-label="Иерархия экранов"></nav>
1659
+ <footer class="sidebar-meta">{audit_markup}{warning_markup}</footer>
1660
+ </aside>
1661
+ <div class="panel-resizer left-resizer" data-resize="left" role="separator" aria-label="Изменить ширину дерева экранов" aria-orientation="vertical" tabindex="0"></div>
1662
+ <main class="workspace">
1663
+ <header class="workbench-header">
1664
+ <div class="menu-bar">
1665
+ <div class="app-mark">{icon("logo", "app-logo")}<span>UI Design Workbench</span><span class="revision-badge" title="Ревизия макета"></span></div>
1666
+ <div class="screen-context"><div class="screen-title"></div><span class="navigation-preview-label" aria-live="polite"></span></div>
1667
+ <div class="menu-spacer"></div>
1668
+ <label class="header-version-picker" title="Какой макет сейчас показан">{icon("compare")}<select class="version-select" aria-label="Какой макет сейчас показан"></select></label><span class="version-application-status" role="status" hidden></span>
1669
+ </div>
1670
+ </header>
1671
+ <section class="stage" aria-label="Холст UI Design Workbench"></section>
1672
+ <div class="canvas-tools" aria-label="Инструменты холста">
1673
+ <div class="mode-switch" aria-label="Режим просмотра">
1674
+ <button type="button" class="mode-button" data-view="overview" aria-pressed="true" aria-label="Все экраны" title="Все экраны">{icon("grid")}<span class="button-label">Макеты</span></button>
1675
+ <button type="button" class="mode-button" data-view="prototype" aria-pressed="false" aria-label="Интерактивный прототип" title="Интерактивный прототип">{icon("play")}<span class="button-label">Прототип</span></button>
1676
+ <button type="button" class="mode-button" data-view="single" aria-pressed="false" aria-label="Один экран" title="Один экран">{icon("frame")}<span class="button-label">Экран</span></button>
1677
+ <button type="button" class="mode-button" data-view="states" aria-pressed="false" aria-label="Все варианты текущего экрана" title="Варианты экрана" disabled>{icon("grid")}<span class="button-label">Варианты</span></button>
1678
+ <button type="button" class="mode-button" data-view="compare" aria-pressed="false" aria-label="Сравнение версий" title="Сравнение версий">{icon("compare")}<span class="button-label">Сравнить</span></button>
1679
+ </div>
1680
+ <span class="tool-divider"></span>
1681
+ <div class="interaction-switch" aria-label="Инструмент">
1682
+ <button type="button" class="interaction-button" data-interaction="interact" aria-pressed="true" aria-label="Взаимодействовать с макетом" title="Взаимодействовать">{icon("pointer")}</button>
1683
+ </div>
1684
+ </div>
1685
+ <div class="screen-state-controls" aria-label="Варианты экрана" hidden></div>
1686
+ <div class="compare-workspace-controls" aria-label="Настройки сравнения">
1687
+ <label class="compare-version-picker"><span>Слева</span><select class="compare-version-select compare-base-select" aria-label="Макет слева для сравнения"></select></label>
1688
+ <span class="compare-version-arrow" aria-hidden="true">↔</span>
1689
+ <label class="compare-version-picker"><span>Справа</span><select class="compare-version-select compare-target-select" aria-label="Макет справа для сравнения"></select></label>
1690
+ <span class="canvas-layer-divider"></span>
1691
+ <div class="compare-layout-controls" aria-label="Способ сравнения"><button type="button" class="compare-button" data-compare-mode="split">Рядом</button><button type="button" class="compare-button" data-compare-mode="overlay">Наложение</button></div>
1692
+ </div>
1693
+ <div class="floating-zoom" aria-label="Масштаб макета" title="Колесо мыши масштабирует холст; зажатое колёсико перемещает холст">
1694
+ <button type="button" class="zoom-button" data-zoom="out" aria-label="Уменьшить масштаб" title="Уменьшить">{icon("minus")}</button><button type="button" class="zoom-button zoom-label" data-zoom="reset" aria-label="Масштаб 100%" title="Сбросить масштаб">100%</button><button type="button" class="zoom-button" data-zoom="in" aria-label="Увеличить масштаб" title="Увеличить">{icon("plus")}</button><button type="button" class="zoom-button" data-zoom="fit" aria-label="Вписать макет" title="Вписать макет">{icon("fit")}</button>
1695
+ </div>
1696
+ <div class="workbench-toast" role="status" aria-live="polite"></div>
1697
+ </main>
1698
+ <div class="panel-resizer right-resizer" data-resize="right" role="separator" aria-label="Изменить ширину панели ревью" aria-orientation="vertical" tabindex="0"></div>
1699
+ <aside class="inspector">
1700
+ <header class="inspector-header">
1701
+ <div class="inspector-context"><span class="inspector-context-icon">{icon("review")}</span><strong class="inspector-context-title">Ревью</strong><span class="inspector-context-count"></span></div>
1702
+ <button type="button" class="panel-close inspector-toggle" aria-label="Скрыть правую панель" title="Скрыть панель">{icon("chevron-right")}</button>
1703
+ </header>
1704
+ <section class="inspector-pane" data-inspector-pane="inspect" hidden>
1705
+ <div class="inspect-body"><div class="panel-empty">{icon("inspect", "empty-icon")}<strong>Выберите элемент</strong><span>Переключитесь на «Свойства» и нажмите элемент на холсте.</span></div></div>
1706
+ </section>
1707
+ <section class="inspector-pane" data-inspector-pane="review">
1708
+ <nav class="review-subtabs" aria-label="Этапы ревью"><button type="button" class="review-subtab" data-review-section="summary" aria-pressed="true"><span class="review-subtab-step">1</span><span><b>Проверка</b><small>Найти проблемы</small></span></button><button type="button" class="review-subtab" data-review-section="problems" aria-pressed="false"><span class="review-subtab-step">2</span><span><b>Проблемы</b><small><span class="finding-total">0</span> найдено</small></span></button><button type="button" class="review-subtab" data-review-section="changes" aria-pressed="false"><span class="review-subtab-step">3</span><span><b>Исправление</b><small><span class="fix-queue-count">0</span> выбрано</small></span></button></nav>
1709
+ <div class="revision-notice" hidden><b>Доступна новая ревизия</b><div>Перенесите совместимые решения или начните чистое ревью.</div><div class="revision-notice-actions"><button type="button" class="migrate-review-state">Перенести</button><button type="button" class="discard-review-state">Сбросить</button></div></div>
1710
+ <section class="review-workspace" data-review-workspace="summary">
1711
+ <section class="review-launcher" data-state="idle" aria-label="Запуск ревью макетов">
1712
+ <div class="review-launcher-head"><span class="review-launcher-icon">{icon("activity")}</span><div><h2 class="review-launcher-title">Ревью макетов</h2><p class="review-launcher-meta"></p></div></div>
1713
+ <div class="review-scope-row"><span class="review-scope-label">Область проверки</span><select class="review-scope-select" aria-label="Область ревью"><option value="all">Все экраны</option><option value="current">Текущий экран</option></select></div>
1714
+ <div class="review-launcher-status" role="status" aria-live="polite"></div><div class="review-launcher-progress" aria-hidden="true"></div>
1715
+ <details class="review-advanced-tools"><summary>Дополнительно</summary><p>Углублённый анализ и технический файл нужны только при сложных случаях.</p><div class="review-launcher-actions"><button type="button" class="review-launcher-action open-agent-review" disabled>{icon("external")}<span>Углублённый анализ</span></button><button type="button" class="review-launcher-action icon-only export-review-request" disabled aria-label="Скачать технический файл" title="Скачать технический файл">{icon("download")}</button></div></details>
1716
+ </section>
1717
+ <details class="coverage-panel" aria-label="Подробности проверки"><summary><span>Что проверено</span><span class="coverage-status"></span></summary><div class="coverage-body"></div></details>
1718
+ <details class="review-history" aria-label="История проверок"><summary><span>Предыдущие проверки</span></summary><div class="history-head"><input class="review-session-name" aria-label="Название проверки" placeholder="Название проверки"><button type="button" class="history-clear">Очистить</button></div><div class="review-history-list"></div></details>
1719
+ <details class="panel-section runtime-diagnostics">
1720
+ <summary><span>Технические результаты</span><span class="diagnostics-status">не запускалась</span></summary>
1721
+ <div class="diagnostics-body"><div class="diagnostics-actions"><button type="button" class="diagnostics-action primary run-diagnostics">{icon("activity")}Запустить</button><button type="button" class="diagnostics-action clear-diagnostics">Очистить</button></div><div class="diagnostics-progress" role="status" aria-live="polite"></div><div class="diagnostics-summary"></div><div class="diagnostics-results"></div></div>
1722
+ </details>
1723
+ </section>
1724
+ <section class="review-workspace automated-review" data-review-workspace="problems" aria-label="Проблемы" hidden>
1725
+ <div class="review-section-heading"><h2>Проблемы</h2><div class="review-section-actions"><span class="finding-total">0</span><button type="button" class="canvas-layer-toggle" data-canvas-layer="findings" aria-pressed="true" aria-label="Показывать проблемы на макете" title="Показать или скрыть проблемы">{icon("review")}<span class="canvas-layer-count">0</span></button></div></div><div class="audit-summary"></div>
1726
+ <details class="finding-filter-panel"><summary>Фильтры</summary><div class="finding-toolbar"><select class="finding-select finding-source-select" aria-label="Источник проблемы"><option value="all">Все источники</option><option value="expert">Экспертный аудит</option><option value="runtime">Автопроверка</option><option value="systemic">Системные</option></select><select class="finding-select finding-screen-select" aria-label="Экран"><option value="all">Все экраны</option></select></div><div class="finding-filters" aria-label="Фильтр замечаний"><button type="button" class="finding-filter" data-finding-filter="all" aria-pressed="true">Все</button><button type="button" class="finding-filter" data-finding-filter="accepted" aria-pressed="false">Выбрано</button><button type="button" class="finding-filter" data-finding-filter="blocker" aria-pressed="false">Критичные</button><button type="button" class="finding-filter" data-finding-filter="high" aria-pressed="false">Важные</button><button type="button" class="finding-filter" data-finding-filter="medium" aria-pressed="false">Обычные</button><button type="button" class="finding-filter" data-finding-filter="low" aria-pressed="false">Низкие</button></div></details>
1727
+ <div class="finding-bulk"><span class="finding-selection-count" role="status">Ничего не выбрано</span><button type="button" class="clear-finding-decisions">Снять выбор</button></div><div class="finding-context" hidden></div><div class="finding-list"></div><details class="review-danger-zone"><summary>Исправить без предварительного просмотра</summary><p>Этот путь сразу подготовит изменение файлов проекта.</p><button type="button" class="fix-all-source">Исправить всё сразу в проекте</button></details>
1728
+ </section>
1729
+ <section class="review-workspace" data-review-workspace="changes" aria-label="Изменения" hidden>
1730
+ <details class="fix-queue" open><summary><span>Текущее состояние</span><span class="fix-queue-count">0</span></summary><div class="fix-queue-body"><div class="fix-queue-stats"></div><div class="fix-queue-actions"><button type="button" class="fix-queue-action copy-fix-request" disabled>{icon("copy")}Скопировать задание</button></div></div></details>
1731
+ <section class="agent-handoff-panel" hidden><div class="agent-handoff-head">{icon("external")}<span class="agent-handoff-status"></span></div><p class="agent-handoff-meta"></p><div class="agent-handoff-actions"><button type="button" class="agent-handoff-action copy-agent-task">Скопировать запрос</button><button type="button" class="agent-handoff-action refresh-preview">Обновить макет</button></div></section>
1732
+ <details class="import-review-panel"><summary>Технические действия</summary><div class="import-review-head"><h3>Обмен с внешним агентом</h3><span class="import-review-badge">не импортирован</span></div><p class="import-review-copy">Используйте только если исправление выполняется в другом инструменте.</p><div class="import-review-actions"><button type="button" class="import-review-action import-review-result">Импортировать JSON</button><button type="button" class="import-review-action export-review-request" disabled>Скачать задание</button></div><input type="file" class="import-review-file" accept="application/json,.json" hidden><div class="import-review-status" role="status"></div></details>
1733
+ <div class="review-decision">Ожидает решения</div>
1734
+ </section>
1735
+ <footer class="review-context-footer"><div class="review-context-status"></div><div class="review-context-actions"><button type="button" class="review-next-action">Запустить ревью</button><button type="button" class="review-secondary-action review-reject-action" hidden>На доработку</button></div></footer>
1736
+ </section>
1737
+ <section class="inspector-pane" data-inspector-pane="comments" hidden>
1738
+ <div class="comment-composer"><div class="panel-empty compact">{icon("comment", "empty-icon")}<strong>Выберите элемент</strong><span>Комментарий будет привязан к выбранному узлу.</span></div></div>
1739
+ <div class="annotation-list"></div>
1740
+ </section>
1741
+ </aside>
1742
+ </div>
1743
+ <script id="ui-preview-context" type="application/json">{encoded_context}</script>
1744
+ <script id="ui-ir-data" type="application/json">{encoded_ir}</script>
1745
+ <script id="runtime-diagnostics-report" type="application/json">null</script>
1746
+ <script>{JS}</script>
1747
+ </body>
1748
+ </html>
1749
+ """
1750
+
1751
+
1752
+ def parse_args() -> argparse.Namespace:
1753
+ parser = argparse.ArgumentParser(description="Render ui-ir.json to a standalone interactive HTML preview.")
1754
+ parser.add_argument("ir", type=Path, help="Path to ui-ir.json")
1755
+ parser.add_argument("--output", type=Path, required=True, help="Output HTML path")
1756
+ parser.add_argument("--allow-draft", action="store_true", help="Render inventory or incomplete IR for diagnostics")
1757
+ parser.add_argument(
1758
+ "--agent",
1759
+ choices=("generic", "codex"),
1760
+ default="generic",
1761
+ help="Optional handoff adapter. Generic copies/downloads a portable job; codex also enables codex://new.",
1762
+ )
1763
+ return parser.parse_args()
1764
+
1765
+
1766
+ def main() -> int:
1767
+ args = parse_args()
1768
+ try:
1769
+ ir = json.loads(args.ir.read_text(encoding="utf-8"))
1770
+ except (OSError, json.JSONDecodeError) as exc:
1771
+ print(f"Cannot read IR: {exc}", file=sys.stderr)
1772
+ return 2
1773
+ errors = validate(ir)
1774
+ if errors:
1775
+ for error in errors:
1776
+ print(error, file=sys.stderr)
1777
+ return 2
1778
+ audit = fidelity_audit(ir)
1779
+ if audit["status"] == "blocked" and not args.allow_draft:
1780
+ print("Preview blocked by fidelity audit:", file=sys.stderr)
1781
+ for reason in audit["reasons"]:
1782
+ print(f"- {reason}", file=sys.stderr)
1783
+ print("Translate the discovered UI from source or pass --allow-draft for diagnostics only.", file=sys.stderr)
1784
+ return 3
1785
+ ir["fidelityAudit"] = audit
1786
+ if audit["reasons"]:
1787
+ ir.setdefault("warnings", []).extend(audit["reasons"])
1788
+ rendered_ir = resolve_assets(ir)
1789
+ output = args.output.resolve()
1790
+ output.parent.mkdir(parents=True, exist_ok=True)
1791
+ output.write_text(render_html(rendered_ir, output.parent, args.agent), encoding="utf-8")
1792
+ print(str(output))
1793
+ return 0
1794
+
1795
+
1796
+ if __name__ == "__main__":
1797
+ raise SystemExit(main())