parapet-rails 0.1.0

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 (231) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +48 -0
  4. data/Rakefile +7 -0
  5. data/app/assets/config/parapet/grammar-registry.json +52 -0
  6. data/app/assets/images/parapet/icons/activities/component-inspector.svg +4 -0
  7. data/app/assets/images/parapet/icons/activities/coverage-heatmap.svg +3 -0
  8. data/app/assets/images/parapet/icons/activities/discovery-sidebar.svg +3 -0
  9. data/app/assets/images/parapet/icons/activities/environment-toggles.svg +4 -0
  10. data/app/assets/images/parapet/icons/activities/failure-inspector.svg +5 -0
  11. data/app/assets/images/parapet/icons/activities/flaky-history.svg +3 -0
  12. data/app/assets/images/parapet/icons/activities/flight-recorder.svg +5 -0
  13. data/app/assets/images/parapet/icons/activities/health-graph.svg +3 -0
  14. data/app/assets/images/parapet/icons/activities/injection-engine.svg +3 -0
  15. data/app/assets/images/parapet/icons/activities/live-sql-stream.svg +5 -0
  16. data/app/assets/images/parapet/icons/activities/live-web-preview.svg +4 -0
  17. data/app/assets/images/parapet/icons/activities/monaco-editor.svg +5 -0
  18. data/app/assets/images/parapet/icons/activities/progress-area.svg +3 -0
  19. data/app/assets/images/parapet/icons/activities/registry-metadata.svg +5 -0
  20. data/app/assets/images/parapet/icons/activities/seed-inspector.svg +4 -0
  21. data/app/assets/images/parapet/icons/activities/slow-test-bottlenecks.svg +4 -0
  22. data/app/assets/images/parapet/icons/activities/smart-search.svg +4 -0
  23. data/app/assets/images/parapet/icons/activities/snippet-library.svg +4 -0
  24. data/app/assets/images/parapet/icons/activities/terminal.svg +4 -0
  25. data/app/assets/images/parapet/icons/activities/unified-schema-view.svg +4 -0
  26. data/app/assets/images/parapet/icons/activities/visual-comparison.svg +3 -0
  27. data/app/assets/images/parapet/icons/activities/visual-summary-bar.svg +5 -0
  28. data/app/assets/images/parapet/icons/nav/ideas.svg +4 -0
  29. data/app/assets/images/parapet/icons/nav/languages.svg +8 -0
  30. data/app/assets/images/parapet/icons/nav/profile.svg +4 -0
  31. data/app/assets/images/parapet/icons/nav/pulse.svg +7 -0
  32. data/app/assets/images/parapet/icons/nav/settings.svg +4 -0
  33. data/app/assets/images/parapet/icons/nav/state.svg +5 -0
  34. data/app/assets/images/parapet/icons/nav/styles.svg +7 -0
  35. data/app/assets/images/parapet/icons/nav/tests.svg +4 -0
  36. data/app/assets/images/parapet/icons/nav/traffic.svg +12 -0
  37. data/app/assets/javascripts/parapet/activity-card.js +439 -0
  38. data/app/assets/javascripts/parapet/activity-cards.js +291 -0
  39. data/app/assets/javascripts/parapet/activity-registry.js +181 -0
  40. data/app/assets/javascripts/parapet/activity-toggles.js +76 -0
  41. data/app/assets/javascripts/parapet/application.js +37 -0
  42. data/app/assets/javascripts/parapet/card-failure-inspector.js +134 -0
  43. data/app/assets/javascripts/parapet/card-file-editor.js +1722 -0
  44. data/app/assets/javascripts/parapet/card-progress-area.js +297 -0
  45. data/app/assets/javascripts/parapet/card-smart-search.js +114 -0
  46. data/app/assets/javascripts/parapet/card-snippet-library.js +251 -0
  47. data/app/assets/javascripts/parapet/explorer-resizer.js +92 -0
  48. data/app/assets/javascripts/parapet/parapet_qss.js +342 -0
  49. data/app/assets/javascripts/parapet/proficiency-filter.js +53 -0
  50. data/app/assets/javascripts/parapet/sidebar-resizer.js +197 -0
  51. data/app/assets/javascripts/parapet/styles-workbench.js +819 -0
  52. data/app/assets/javascripts/parapet/tree-logic.js +320 -0
  53. data/app/assets/stylesheets/parapet/_qss_variables.scss +122 -0
  54. data/app/assets/stylesheets/parapet/parapet_qss.scss +69 -0
  55. data/app/assets/stylesheets/parapet/qss_application.css +260940 -0
  56. data/app/assets/stylesheets/parapet/qss_application.css.map +1 -0
  57. data/app/assets/stylesheets/parapet/qss_application.scss +5 -0
  58. data/app/assets/stylesheets/parapet/qss_workspace.scss +209 -0
  59. data/app/assets/stylesheets/parapet/sample.qss +22 -0
  60. data/app/controllers/parapet/application_controller.rb +54 -0
  61. data/app/controllers/parapet/base_controller.rb +18 -0
  62. data/app/controllers/parapet/build_suite/dashboard_controller.rb +10 -0
  63. data/app/controllers/parapet/css_suite/compiler_controller.rb +95 -0
  64. data/app/controllers/parapet/css_suite/dashboard_controller.rb +10 -0
  65. data/app/controllers/parapet/css_suite/preview_controller.rb +46 -0
  66. data/app/controllers/parapet/dashboard_controller.rb +8 -0
  67. data/app/controllers/parapet/drafts_controller.rb +30 -0
  68. data/app/controllers/parapet/explorers_controller.rb +53 -0
  69. data/app/controllers/parapet/files_controller.rb +208 -0
  70. data/app/controllers/parapet/footers_controller.rb +24 -0
  71. data/app/controllers/parapet/intentions_suite/dashboard_controller.rb +10 -0
  72. data/app/controllers/parapet/landing_controller.rb +14 -0
  73. data/app/controllers/parapet/monitor_suite/dashboard_controller.rb +10 -0
  74. data/app/controllers/parapet/pm_suite/publish_controller.rb +16 -0
  75. data/app/controllers/parapet/polyglot_suite/dashboard_controller.rb +10 -0
  76. data/app/controllers/parapet/runner_controller.rb +34 -0
  77. data/app/controllers/parapet/sessions_controller.rb +35 -0
  78. data/app/controllers/parapet/signal_suite/dashboard_controller.rb +10 -0
  79. data/app/controllers/parapet/test_suite/dashboard_controller.rb +10 -0
  80. data/app/controllers/parapet/widgets_controller.rb +33 -0
  81. data/app/helpers/parapet/application_helper.rb +23 -0
  82. data/app/helpers/parapet/icon_helper.rb +57 -0
  83. data/app/jobs/parapet/application_job.rb +4 -0
  84. data/app/mailers/parapet/application_mailer.rb +6 -0
  85. data/app/models/parapet/activity.rb +14 -0
  86. data/app/models/parapet/application_record.rb +5 -0
  87. data/app/models/parapet/current.rb +6 -0
  88. data/app/models/parapet/draft_recovery.rb +9 -0
  89. data/app/models/parapet/internal_record.rb +34 -0
  90. data/app/models/parapet/run_history.rb +22 -0
  91. data/app/models/parapet/runtime_setting.rb +8 -0
  92. data/app/models/parapet/session.rb +7 -0
  93. data/app/models/parapet/suite_assignment.rb +30 -0
  94. data/app/models/parapet/user.rb +12 -0
  95. data/app/services/parapet/activity_phrase_renderer.rb +35 -0
  96. data/app/services/parapet/activity_writer.rb +26 -0
  97. data/app/services/parapet/authorization.rb +198 -0
  98. data/app/services/parapet/consistency_auditor.rb +58 -0
  99. data/app/services/parapet/directory_scanner.rb +159 -0
  100. data/app/services/parapet/draft_recovery_store.rb +18 -0
  101. data/app/services/parapet/explorer_manager.rb +402 -0
  102. data/app/services/parapet/file_save_service.rb +21 -0
  103. data/app/services/parapet/git_side_ref_committer.rb +50 -0
  104. data/app/services/parapet/noun_discovery.rb +26 -0
  105. data/app/services/parapet/publish_gate.rb +83 -0
  106. data/app/services/parapet/test_runner.rb +178 -0
  107. data/app/views/layouts/parapet/application.html.erb +83 -0
  108. data/app/views/layouts/parapet/landing.html.erb +19 -0
  109. data/app/views/parapet/activities/_ai_error_explainer.html.erb +33 -0
  110. data/app/views/parapet/activities/_ai_explainer_widget.html.erb +20 -0
  111. data/app/views/parapet/activities/_calendar_widget.html.erb +31 -0
  112. data/app/views/parapet/activities/_code_snippets_widget.html.erb +22 -0
  113. data/app/views/parapet/activities/_coverage_explorer.html.erb +1 -0
  114. data/app/views/parapet/activities/_dropdown_test.html.erb +20 -0
  115. data/app/views/parapet/activities/_explorer_node.html.erb +102 -0
  116. data/app/views/parapet/activities/_explorer_tree.html.erb +16 -0
  117. data/app/views/parapet/activities/_file_editor.html.erb +183 -0
  118. data/app/views/parapet/activities/_file_explorer.html.erb +23 -0
  119. data/app/views/parapet/activities/_fix_suggestions_widget.html.erb +13 -0
  120. data/app/views/parapet/activities/_generic.html.erb +6 -0
  121. data/app/views/parapet/activities/_live_preview.html.erb +13 -0
  122. data/app/views/parapet/activities/_perspective_filters.html.erb +36 -0
  123. data/app/views/parapet/activities/_progress_area.html.erb +37 -0
  124. data/app/views/parapet/activities/_project_explorer.html.erb +1 -0
  125. data/app/views/parapet/activities/_registry_explorer.html.erb +1 -0
  126. data/app/views/parapet/activities/_run_history.html.erb +2 -0
  127. data/app/views/parapet/activities/_smart_search.html.erb +11 -0
  128. data/app/views/parapet/activities/file_explorer/_perspective_scripts.html.erb +58 -0
  129. data/app/views/parapet/activities/file_explorer/_perspectives.html.erb +0 -0
  130. data/app/views/parapet/activities/file_explorer/_viewport_scripts.html.erb +33 -0
  131. data/app/views/parapet/css_suite/_workbench.html.erb +51 -0
  132. data/app/views/parapet/dashboards/index.html.erb +113 -0
  133. data/app/views/parapet/debug_panel.html.erb +79 -0
  134. data/app/views/parapet/debug_panel_tailwind.html.erb +98 -0
  135. data/app/views/parapet/erb_patch_test.html.erb +5 -0
  136. data/app/views/parapet/explorers/show.html.erb +1 -0
  137. data/app/views/parapet/git_viewer.html.erb +62 -0
  138. data/app/views/parapet/git_viewer_after.html.erb +14 -0
  139. data/app/views/parapet/git_viewer_before.html.erb +14 -0
  140. data/app/views/parapet/git_viewer_governed.html.erb +88 -0
  141. data/app/views/parapet/git_viewer_governed_after.html.erb +36 -0
  142. data/app/views/parapet/git_viewer_governed_before.html.erb +36 -0
  143. data/app/views/parapet/git_viewer_tailwind.html.erb +88 -0
  144. data/app/views/parapet/git_viewer_thin_wrapper.html.erb +85 -0
  145. data/app/views/parapet/hierarchical_layout_test.html.erb +85 -0
  146. data/app/views/parapet/hybrid_stress_test.html.erb +25 -0
  147. data/app/views/parapet/index.html.erb +35 -0
  148. data/app/views/parapet/mutation_stress_test.html.erb +82 -0
  149. data/app/views/parapet/recursive_anchoring_test.html.erb +66 -0
  150. data/app/views/parapet/sessions/new.html.erb +28 -0
  151. data/app/views/parapet/shared/_cad_inspector.html.erb +92 -0
  152. data/app/views/parapet/shared/_discovery_sidebar.html.erb +48 -0
  153. data/app/views/parapet/shared/_explorer_bar.html.erb +71 -0
  154. data/app/views/parapet/shared/_footer.html.erb +10 -0
  155. data/app/views/parapet/shared/_footer_ideas.html.erb +36 -0
  156. data/app/views/parapet/shared/_footer_languages.html.erb +33 -0
  157. data/app/views/parapet/shared/_footer_pulse.html.erb +33 -0
  158. data/app/views/parapet/shared/_footer_state.html.erb +33 -0
  159. data/app/views/parapet/shared/_footer_styles.html.erb +37 -0
  160. data/app/views/parapet/shared/_footer_tests.html.erb +39 -0
  161. data/app/views/parapet/shared/_footer_traffic.html.erb +45 -0
  162. data/app/views/parapet/shared/_global_nav.html.erb +55 -0
  163. data/app/views/parapet/shared/_head.html.erb +4 -0
  164. data/app/views/parapet/shared/_header.html.erb +20 -0
  165. data/app/views/parapet/shared/_object_inspector.html.erb +76 -0
  166. data/app/views/parapet/shared/_ribbon_header.html.erb +3 -0
  167. data/app/views/parapet/shared/_root_selector_modal.html.erb +15 -0
  168. data/app/views/parapet/shared/_tab_bar.html.erb +62 -0
  169. data/app/views/parapet/shared/activities_ribbon/_agent_group.html.erb +15 -0
  170. data/app/views/parapet/shared/activities_ribbon/_analysis_group.html.erb +15 -0
  171. data/app/views/parapet/shared/activities_ribbon/_architect_group.html.erb +21 -0
  172. data/app/views/parapet/shared/activities_ribbon/_explorer_group.html.erb +12 -0
  173. data/app/views/parapet/shared/activities_ribbon/_inspector_group.html.erb +15 -0
  174. data/app/views/parapet/shared/activities_ribbon/_proficiency_filter.html.erb +24 -0
  175. data/app/views/parapet/shared/activities_ribbon/_runner_group.html.erb +21 -0
  176. data/app/views/parapet/shared/activities_ribbon/_spatial_inspector_group.html.erb +18 -0
  177. data/app/views/parapet/shared/activities_ribbon/_studio_group.html.erb +18 -0
  178. data/app/views/parapet/shared/activities_ribbon/_stylist_group.html.erb +18 -0
  179. data/app/views/parapet/shared/explorers/_base.html.erb +107 -0
  180. data/app/views/parapet/shared/explorers/_css.html.erb +1 -0
  181. data/app/views/parapet/shared/explorers/_intentions.html.erb +1 -0
  182. data/app/views/parapet/shared/explorers/_project.html.erb +1 -0
  183. data/app/views/parapet/shared/explorers/_tests.html.erb +1 -0
  184. data/app/views/parapet/shared/head/_meta.html.erb +3 -0
  185. data/app/views/parapet/shared/head/_styles_activities.html.erb +152 -0
  186. data/app/views/parapet/shared/head/_styles_shared.html.erb +52 -0
  187. data/app/views/parapet/shared/head/_styles_workspace.html.erb +67 -0
  188. data/app/views/parapet/shared/ribbon_header/_diagnostic_controls.html.erb +17 -0
  189. data/app/views/parapet/shared/ribbon_header/_execution_controls.html.erb +28 -0
  190. data/app/views/parapet/shared/ribbon_header/_focus_search.html.erb +5 -0
  191. data/app/views/parapet/shared/root_selector/_header.html.erb +10 -0
  192. data/app/views/parapet/shared/root_selector/_path_input.html.erb +13 -0
  193. data/app/views/parapet/shared/root_selector/_quick_select.html.erb +20 -0
  194. data/app/views/parapet/shared/root_selector/_scripts.html.erb +61 -0
  195. data/app/views/parapet/shared/root_selector/_warning.html.erb +11 -0
  196. data/app/views/parapet/shared/widgets/_base.html.erb +6 -0
  197. data/app/views/parapet/shared/widgets/_css.html.erb +79 -0
  198. data/app/views/parapet/widgets/_explainer.html.erb +17 -0
  199. data/app/views/parapet/widgets/_hotkeys.html.erb +20 -0
  200. data/app/views/parapet/widgets/_search.html.erb +68 -0
  201. data/app/views/parapet/widgets/_spatial_inspector.html.erb +54 -0
  202. data/app/views/parapet/widgets/_widget-explainer.html.erb +17 -0
  203. data/app/views/parapet/widgets/_widget-hotkeys.html.erb +20 -0
  204. data/app/views/parapet/widgets/_widget-search.html.erb +68 -0
  205. data/app/views/parapet/widgets/_widget-snippets.html.erb +18 -0
  206. data/app/views/parapet/widgets/_widget-suggestions.html.erb +15 -0
  207. data/app/views/parapet/widgets/show.html.erb +3 -0
  208. data/app/views/shared/_footer.html.erb +0 -0
  209. data/config/parapet/qss_config.yml +117 -0
  210. data/config/parapet/qss_registry.yml +14 -0
  211. data/config/routes.rb +40 -0
  212. data/db/migrate/20260916035300_create_run_histories.rb +16 -0
  213. data/db/migrate/20261002000000_expand_run_histories_for_activity_tracking.rb +7 -0
  214. data/db/parapet_migrate/20260929184200_create_parapet_users.rb +12 -0
  215. data/db/parapet_migrate/20260929184300_create_parapet_sessions.rb +9 -0
  216. data/db/parapet_migrate/20260930000000_create_parapet_runtime_settings.rb +12 -0
  217. data/db/parapet_migrate/20260930000100_create_parapet_suite_assignments.rb +14 -0
  218. data/db/parapet_migrate/20261002000000_create_parapet_activities.rb +16 -0
  219. data/db/parapet_migrate/20261002000100_create_parapet_draft_recoveries.rb +13 -0
  220. data/db/parapet_schema.rb +64 -0
  221. data/db/schema.rb +21 -0
  222. data/db/schema.released.rb +0 -0
  223. data/lib/parapet/engine.rb +11 -0
  224. data/lib/parapet/environment_guard.rb +14 -0
  225. data/lib/parapet/version.rb +3 -0
  226. data/lib/parapet-rails.rb +1 -0
  227. data/lib/parapet.rb +59 -0
  228. data/lib/tasks/parapet_qss.rake +26 -0
  229. data/lib/tasks/parapet_tasks.rake +25 -0
  230. data/lib/tasks/randomize_explorer_health.rb +101 -0
  231. metadata +423 -0
@@ -0,0 +1,819 @@
1
+ // Styles Workbench — Logic for the Spatial CAD design environment
2
+ (function() {
3
+ let monacoSource = null;
4
+ let monacoCompiled = null;
5
+ let currentPath = null;
6
+
7
+ function init() {
8
+ const workbench = document.getElementById('pp-styles-workbench');
9
+ if (!workbench || workbench.dataset.initialized) return;
10
+ workbench.dataset.initialized = 'true';
11
+
12
+ console.log("Styles Workbench: [1] Initializing workbench components...");
13
+
14
+ // 1. Initialize Monaco Editors
15
+ if (typeof require !== 'undefined') {
16
+ require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' } });
17
+ require(['vs/editor/editor.main'], function(monacoInstance) {
18
+ window.monaco = monacoInstance;
19
+ const sourceContainer = document.getElementById('pp-monaco-source');
20
+ const compiledContainer = document.getElementById('pp-monaco-compiled');
21
+
22
+ console.log("Styles Workbench: [2] Monaco library loaded.");
23
+
24
+ if (sourceContainer) {
25
+ monacoSource = monacoInstance.editor.create(sourceContainer, {
26
+ value: "",
27
+ language: 'handlebars',
28
+ theme: 'vs-dark',
29
+ automaticLayout: true,
30
+ minimap: { enabled: false },
31
+ fontSize: 11,
32
+ lineNumbers: 'on',
33
+ scrollBeyondLastLine: false,
34
+ roundedSelection: false,
35
+ readOnly: false,
36
+ backgroundColor: '#0f1115'
37
+ });
38
+
39
+ // Trigger compilation on change
40
+ let debounceTimer = null;
41
+ monacoSource.onDidChangeModelContent(() => {
42
+ clearTimeout(debounceTimer);
43
+ debounceTimer = setTimeout(() => {
44
+ const content = monacoSource.getValue();
45
+ const lang = monacoSource.getModel().getLanguageId();
46
+
47
+ if (lang === 'handlebars' && currentPath && currentPath.endsWith('.qss')) {
48
+ // Direct QSS Transpilation (Zero-Latency)
49
+ const css = QSSEngine.transpile(content);
50
+ if (monacoCompiled) monacoCompiled.setValue(css);
51
+ injectLiveStyles(css);
52
+ } else if (lang === 'scss' || lang === 'css') {
53
+ compileScss(content);
54
+ }
55
+ }, 300); // Faster debounce for local QSS
56
+ });
57
+ }
58
+
59
+ if (compiledContainer) {
60
+ monacoCompiled = monacoInstance.editor.create(compiledContainer, {
61
+ value: "",
62
+ language: 'css',
63
+ theme: 'vs-dark',
64
+ automaticLayout: true,
65
+ minimap: { enabled: false },
66
+ fontSize: 11,
67
+ lineNumbers: 'on',
68
+ scrollBeyondLastLine: false,
69
+ roundedSelection: false,
70
+ readOnly: true
71
+ });
72
+ }
73
+ console.log("Styles Workbench: [3] Monaco instances created.");
74
+ });
75
+ }
76
+
77
+ // 2. Listen for File Selection (Global Dispatch)
78
+ window._qssFileSelectedListener = function(e) {
79
+ const path = e.detail.path;
80
+ if (!path) return;
81
+
82
+ console.log("Styles Workbench: [Event] parapet:fileSelected ->", path);
83
+
84
+ // Verify the workbench is still in the DOM and visible
85
+ const currentWorkbench = document.getElementById('pp-styles-workbench');
86
+ if (!currentWorkbench) {
87
+ console.log("Styles Workbench: Workbench element gone, removing listener.");
88
+ document.removeEventListener('parapet:fileSelected', window._qssFileSelectedListener);
89
+ return;
90
+ }
91
+
92
+ const stylesSuite = document.querySelector('.suite-group[data-suites="css"]');
93
+ const isVisible = stylesSuite && (stylesSuite.style.display !== 'none' && !stylesSuite.classList.contains('hidden'));
94
+
95
+ if (!isVisible) {
96
+ console.log("Styles Workbench: Skipping (Styles Suite not active)");
97
+ return;
98
+ }
99
+
100
+ loadFileToWorkbench(path);
101
+ };
102
+
103
+ document.addEventListener('parapet:fileSelected', window._qssFileSelectedListener);
104
+
105
+ // 3. Load Assets Library
106
+ loadAssetsLibrary();
107
+
108
+ // 4. Initialize Sub-Components
109
+ initObjectInspector();
110
+ initConsoleResizer();
111
+ initGrammarPalette();
112
+ initFlushLogic();
113
+ initCommandBar();
114
+
115
+ // Audit Logic (Existing)
116
+ const auditorContent = document.getElementById('pp-auditor-warnings');
117
+ const healthPct = document.querySelector('.qss-text-emerald-500');
118
+
119
+ function runAudit() {
120
+ if (!auditorContent) return;
121
+ const warnings = [];
122
+ const interactiveElements = document.querySelectorAll('.qss-cursor-pointer, button, a');
123
+ interactiveElements.forEach(el => {
124
+ const rect = el.getBoundingClientRect();
125
+ if (rect.width > 0 && (rect.width < 44 || rect.height < 44)) {
126
+ warnings.push({ level: 'red', message: `Usability Floor Violation: ${el.id || el.tagName} is too small.` });
127
+ }
128
+ });
129
+ renderWarnings(warnings);
130
+ }
131
+
132
+ function renderWarnings(warnings) {
133
+ if (!auditorContent) return;
134
+ if (warnings.length === 0) {
135
+ auditorContent.innerHTML = '<div class="qss-p[8] qss-text-emerald-400/60 qss-text-size[9]">Clean Layout</div>';
136
+ return;
137
+ }
138
+ auditorContent.innerHTML = warnings.map(w => `<div class="qss-text-red-400 qss-text-size[9]">- ${w.message}</div>`).join('');
139
+ }
140
+
141
+ runAudit();
142
+ window._qssAuditResizeListener = runAudit;
143
+ window._qssAuditRefreshListener = runAudit;
144
+
145
+ window.addEventListener('resize', window._qssAuditResizeListener);
146
+ document.addEventListener('workbench:refresh', window._qssAuditRefreshListener);
147
+ }
148
+
149
+ async function compileScss(scss) {
150
+ if (!monacoCompiled) return;
151
+
152
+ try {
153
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
154
+ const response = await fetch('/css_suite/compile', {
155
+ method: 'POST',
156
+ headers: {
157
+ 'Content-Type': 'application/json',
158
+ 'X-CSRF-Token': csrfToken
159
+ },
160
+ body: JSON.stringify({ scss: scss })
161
+ });
162
+ const data = await response.json();
163
+
164
+ if (data.error) {
165
+ // TODO: Show error in CSS Inspector console
166
+ console.error("SCSS Compilation Error:", data.error);
167
+ // Keep old CSS but maybe dim it or show a status indicator
168
+ } else {
169
+ monacoCompiled.setValue(data.css);
170
+ injectLiveStyles(data.css);
171
+ }
172
+ } catch (err) {
173
+ console.error("Workbench: Compilation request failed", err);
174
+ }
175
+ }
176
+
177
+ function injectLiveStyles(css) {
178
+ let styleTag = document.getElementById('pp-live-bench');
179
+ if (!styleTag) {
180
+ styleTag = document.createElement('style');
181
+ styleTag.id = 'pp-live-bench';
182
+ document.head.appendChild(styleTag);
183
+ }
184
+ styleTag.textContent = css;
185
+
186
+ // Inject into Shadow Root if present
187
+ const container = document.getElementById('pp-mockup-container');
188
+ if (container && container.shadowRoot) {
189
+ let shadowTag = container.shadowRoot.querySelector('#pp-live-bench-shadow');
190
+ if (!shadowTag) {
191
+ shadowTag = document.createElement('style');
192
+ shadowTag.id = 'pp-live-bench-shadow';
193
+ container.shadowRoot.appendChild(shadowTag);
194
+ }
195
+ shadowTag.textContent = css;
196
+ }
197
+ }
198
+
199
+ async function loadAssetsLibrary() {
200
+ const container = document.getElementById('pp-assets-library');
201
+ if (!container) return;
202
+
203
+ try {
204
+ const response = await fetch('/files/assets');
205
+ const assets = await response.json();
206
+
207
+ if (assets.length === 0) {
208
+ container.innerHTML = '<div class="qss-col-span-4 qss-py[12] qss-text-center qss-text-slate-600 qss-text-size[9]">No assets found</div>';
209
+ return;
210
+ }
211
+
212
+ container.innerHTML = assets.map(asset => `
213
+ <div class="qss-aspect-square qss-bg-slate-900/50 qss-border qss-border-slate-800 qss-rounded qss-flex qss-items-center qss-justify-center qss-cursor-pointer hover:qss-border-indigo-500/50 qss-group qss-relative" title="${asset.name}" data-path="${asset.path}">
214
+ ${asset.path.endsWith('.svg') ?
215
+ `<img src="${asset.url}" class="qss-w[20] qss-h[20] qss-opacity-60 group-hover:qss-opacity-100" />` :
216
+ `<div class="qss-text-size[8] qss-text-slate-600 qss-uppercase">${asset.path.split('.').pop()}</div>`
217
+ }
218
+ </div>
219
+ `).join('');
220
+
221
+ } catch (err) {
222
+ console.error("Workbench: Failed to load assets", err);
223
+ }
224
+ }
225
+
226
+ async function renderLivePreview(path) {
227
+ const mockupContainer = document.getElementById('pp-mockup-container');
228
+ if (!mockupContainer) return;
229
+
230
+ // Ensure Shadow DOM encapsulation
231
+ let shadow = mockupContainer.shadowRoot;
232
+ if (!shadow) {
233
+ shadow = mockupContainer.attachShadow({ mode: 'open' });
234
+ QSSEngine.addRoot(shadow);
235
+ }
236
+
237
+ shadow.innerHTML = `<div class="qss-p[40] qss-text-center qss-text-slate-500">
238
+ <div class="qss-text-size[12] qss-animate-pulse">Rendering Spatial Preview...</div>
239
+ </div>`;
240
+
241
+ try {
242
+ const response = await fetch(`/css_suite/preview?path=${encodeURIComponent(path)}`);
243
+ const html = await response.text();
244
+
245
+ shadow.innerHTML = html;
246
+
247
+ // Re-run scripts if any were injected (standard innerHTML behavior skips them)
248
+ const scripts = shadow.querySelectorAll('script');
249
+ scripts.forEach(oldScript => {
250
+ const newScript = document.createElement('script');
251
+ Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value));
252
+ newScript.appendChild(document.createTextNode(oldScript.innerHTML));
253
+ shadow.appendChild(newScript);
254
+ oldScript.parentNode.removeChild(oldScript);
255
+ });
256
+
257
+ // Re-apply live styles to the new shadow content
258
+ const liveCss = document.getElementById('pp-live-bench')?.textContent;
259
+ if (liveCss) injectLiveStyles(liveCss);
260
+
261
+ } catch (err) {
262
+ console.error("Workbench: Preview failed", err);
263
+ shadow.innerHTML = `<div class="qss-p[20] qss-text-red-400">Failed to render preview.</div>`;
264
+ }
265
+ }
266
+
267
+ async function loadFileToWorkbench(path) {
268
+ console.info("Styles Workbench: [Action] Starting load process for:", path);
269
+ currentPath = path;
270
+ const sourceLabel = document.getElementById('pp-source-filename');
271
+ const mockupContainer = document.getElementById('pp-mockup-container');
272
+
273
+ if (sourceLabel) sourceLabel.textContent = path.split('/').pop();
274
+
275
+ try {
276
+ const contentUrl = `/files/content?path=${encodeURIComponent(path)}`;
277
+ console.log("Styles Workbench: [Step 1] Fetching source from:", contentUrl);
278
+
279
+ const response = await fetch(contentUrl);
280
+ if (!response.ok) throw new Error(`Fetch Error: ${response.status}`);
281
+
282
+ const data = await response.json();
283
+ console.log("Styles Workbench: [Step 2] Content received. Length:", data.content ? data.content.length : 0);
284
+
285
+ if (monacoSource && window.monaco) {
286
+ const ext = path.split('.').pop();
287
+ const langMap = { erb: 'handlebars', scss: 'scss', css: 'css', rb: 'ruby', qss: 'handlebars' };
288
+ window.monaco.editor.setModelLanguage(monacoSource.getModel(), langMap[ext] || 'plaintext');
289
+ monacoSource.setValue(data.content || "");
290
+ console.log("Styles Workbench: [Step 3] Monaco Editor updated.");
291
+
292
+ if (ext === 'qss') {
293
+ console.log("Styles Workbench: [Step 3b] Local QSS Transpilation...");
294
+ const css = QSSEngine.transpile(data.content);
295
+ if (monacoCompiled) monacoCompiled.setValue(css);
296
+ injectLiveStyles(css);
297
+ } else if (ext === 'scss' || ext === 'css') {
298
+ console.log("Styles Workbench: [Step 3c] Triggering SCSS compilation...");
299
+ compileScss(data.content);
300
+ }
301
+ }
302
+
303
+ if (mockupContainer) {
304
+ if (path.endsWith('.erb')) {
305
+ console.info("Styles Workbench: [Step 4] ERB file detected. Requesting Live Preview...");
306
+ renderLivePreview(path);
307
+ } else {
308
+ console.log("Styles Workbench: [Step 4] Non-ERB file. Displaying placeholder in canvas.");
309
+ mockupContainer.innerHTML = `<div class="qss-flex qss-items-center qss-justify-center qss-h-[600px] qss-text-slate-500 qss-italic">
310
+ Style/Logic File Loaded. Select a View (.erb) to see it in action.
311
+ </div>`;
312
+ }
313
+ }
314
+
315
+ } catch (err) {
316
+ console.error("Styles Workbench: [Critical Error] Load failed:", err);
317
+ if (mockupContainer) {
318
+ mockupContainer.innerHTML = `<div class="qss-p[20] qss-text-red-500">
319
+ <strong>Load Error:</strong> ${err.message}
320
+ </div>`;
321
+ }
322
+ }
323
+ }
324
+
325
+ function initCommandBar() {
326
+ const bar = document.getElementById('pp-workbench-command-bar');
327
+ if (!bar) return;
328
+
329
+ // 1. Interaction State Locking
330
+ bar.querySelectorAll('[data-state]').forEach(btn => {
331
+ btn.addEventListener('click', () => {
332
+ const state = btn.dataset.state;
333
+ const isActive = btn.classList.contains('bg-indigo-600');
334
+
335
+ // Clear others
336
+ bar.querySelectorAll('[data-state]').forEach(b => {
337
+ b.classList.remove('bg-indigo-600', 'text-white');
338
+ b.classList.add('bg-slate-800', 'text-slate-400');
339
+ });
340
+
341
+ if (!isActive) {
342
+ btn.classList.add('bg-indigo-600', 'text-white');
343
+ btn.classList.remove('bg-slate-800', 'text-slate-400');
344
+ document.body.dataset.qssStateLock = state;
345
+ } else {
346
+ delete document.body.dataset.qssStateLock;
347
+ }
348
+
349
+ // Trigger preview refresh to apply state
350
+ const mockup = document.getElementById('pp-mockup-container');
351
+ if (mockup && mockup.shadowRoot) {
352
+ // We'll implement state-specific style injection later
353
+ console.log(`State Lock: ${state} ${!isActive ? 'ON' : 'OFF'}`);
354
+ }
355
+ });
356
+ });
357
+
358
+ // 2. Grid Toggles
359
+ bar.querySelectorAll('[data-grid]').forEach(btn => {
360
+ btn.addEventListener('click', () => {
361
+ const size = btn.dataset.grid;
362
+ const isActive = btn.classList.contains('bg-indigo-600');
363
+
364
+ bar.querySelectorAll('[data-grid]').forEach(b => {
365
+ b.classList.remove('bg-indigo-600', 'text-white');
366
+ b.classList.add('bg-slate-800', 'text-slate-400');
367
+ });
368
+
369
+ if (!isActive) {
370
+ btn.classList.add('bg-indigo-600', 'text-white');
371
+ btn.classList.remove('bg-slate-800', 'text-slate-400');
372
+ showGrid(size);
373
+ } else {
374
+ hideGrid();
375
+ }
376
+ });
377
+ });
378
+
379
+ // 3. Anchor Toggle
380
+ const anchorBtn = document.getElementById('pp-toggle-anchors');
381
+ if (anchorBtn) {
382
+ anchorBtn.addEventListener('click', () => {
383
+ const isActive = anchorBtn.classList.contains('bg-indigo-600');
384
+ if (!isActive) {
385
+ anchorBtn.classList.add('bg-indigo-600', 'text-white');
386
+ anchorBtn.classList.remove('bg-slate-800', 'text-slate-400');
387
+ showAnchors();
388
+ } else {
389
+ anchorBtn.classList.remove('bg-indigo-600', 'text-white');
390
+ anchorBtn.classList.add('bg-slate-800', 'text-slate-400');
391
+ hideAnchors();
392
+ }
393
+ });
394
+ }
395
+ }
396
+
397
+ function showGrid(size) {
398
+ const container = document.getElementById('pp-mockup-container');
399
+ if (!container) return;
400
+
401
+ let grid = document.getElementById('pp-spatial-grid');
402
+ if (!grid) {
403
+ grid = document.createElementNS("http://www.w3.org/2000/svg", "svg");
404
+ grid.id = 'pp-spatial-grid';
405
+ grid.style.position = 'absolute';
406
+ grid.style.top = '0';
407
+ grid.style.left = '0';
408
+ grid.style.width = '100%';
409
+ grid.style.height = '100%';
410
+ grid.style.pointerEvents = 'none';
411
+ grid.style.zIndex = '1000';
412
+ grid.style.opacity = '0.2';
413
+ container.appendChild(grid);
414
+ }
415
+
416
+ grid.innerHTML = '';
417
+ const step = parseInt(size);
418
+ const color = size === '10' ? '#4f46e5' : (size === '50' ? '#10b981' : '#f59e0b');
419
+
420
+ for (let i = step; i < 1000; i += step) {
421
+ // Vertical lines
422
+ const vLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
423
+ vLine.setAttribute('x1', `${i/10}%`);
424
+ vLine.setAttribute('y1', '0');
425
+ vLine.setAttribute('x2', `${i/10}%`);
426
+ vLine.setAttribute('y2', '100%');
427
+ vLine.setAttribute('stroke', color);
428
+ vLine.setAttribute('stroke-width', '1');
429
+ grid.appendChild(vLine);
430
+
431
+ // Horizontal lines
432
+ const hLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
433
+ hLine.setAttribute('x1', '0');
434
+ hLine.setAttribute('y1', `${i/10}%`);
435
+ hLine.setAttribute('x2', '100%');
436
+ hLine.setAttribute('y2', `${i/10}%`);
437
+ hLine.setAttribute('stroke', color);
438
+ hLine.setAttribute('stroke-width', '1');
439
+ grid.appendChild(hLine);
440
+ }
441
+ }
442
+
443
+ function hideGrid() {
444
+ const grid = document.getElementById('pp-spatial-grid');
445
+ if (grid) grid.remove();
446
+ }
447
+
448
+ function showAnchors() {
449
+ const container = document.getElementById('pp-mockup-container');
450
+ if (!container) return;
451
+
452
+ let overlay = document.getElementById('pp-anchor-overlay');
453
+ if (!overlay) {
454
+ overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
455
+ overlay.id = 'pp-anchor-overlay';
456
+ overlay.style.position = 'absolute';
457
+ overlay.style.top = '0';
458
+ overlay.style.left = '0';
459
+ overlay.style.width = '100%';
460
+ overlay.style.height = '100%';
461
+ overlay.style.pointerEvents = 'none';
462
+ overlay.style.zIndex = '1001';
463
+ container.appendChild(overlay);
464
+ }
465
+
466
+ // Logic to draw "Laser Lines" for selected element
467
+ const selected = container.shadowRoot?.querySelector('.qss-inspector-selected');
468
+ if (!selected) {
469
+ overlay.innerHTML = '';
470
+ return;
471
+ }
472
+
473
+ overlay.innerHTML = '';
474
+ const rect = selected.getBoundingClientRect();
475
+ const contRect = container.getBoundingClientRect();
476
+
477
+ // Convert to percentage-based coordinates within container
478
+ const x = (rect.left - contRect.left) / contRect.width * 100;
479
+ const y = (rect.top - contRect.top) / contRect.height * 100;
480
+ const w = rect.width / contRect.width * 100;
481
+ const h = rect.height / contRect.height * 100;
482
+
483
+ // Draw crosshair or anchor lines if relational properties exist
484
+ const adjectives = Array.from(selected.classList);
485
+ adjectives.forEach(adj => {
486
+ const match = adj.match(/^(pos-[xyrb])\[\^([^\]]+)\]/);
487
+ if (match) {
488
+ const prop = match[1];
489
+ const anchorId = match[2];
490
+ const anchorEl = container.shadowRoot?.getElementById(anchorId);
491
+ if (anchorEl) {
492
+ const aRect = anchorEl.getBoundingClientRect();
493
+ const ax = (aRect.left - contRect.left) / contRect.width * 100;
494
+ const ay = (aRect.top - contRect.top) / contRect.height * 100;
495
+ const aw = aRect.width / contRect.width * 100;
496
+ const ah = aRect.height / contRect.height * 100;
497
+
498
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
499
+ line.setAttribute('stroke', '#6366f1');
500
+ line.setAttribute('stroke-width', '2');
501
+ line.setAttribute('stroke-dasharray', '4');
502
+
503
+ if (prop === 'pos-x') {
504
+ line.setAttribute('x1', `${ax + aw}%`); line.setAttribute('y1', `${y + h/2}%`);
505
+ line.setAttribute('x2', `${x}%`); line.setAttribute('y2', `${y + h/2}%`);
506
+ } else if (prop === 'pos-y') {
507
+ line.setAttribute('x1', `${x + w/2}%`); line.setAttribute('y1', `${ay + ah}%`);
508
+ line.setAttribute('x2', `${x + w/2}%`); line.setAttribute('y2', `${y}%`);
509
+ }
510
+ overlay.appendChild(line);
511
+ }
512
+ }
513
+ });
514
+ }
515
+
516
+ function hideAnchors() {
517
+ const overlay = document.getElementById('pp-anchor-overlay');
518
+ if (overlay) overlay.remove();
519
+ }
520
+
521
+ function initObjectInspector() {
522
+ const mockupContainer = document.getElementById('pp-mockup-container');
523
+ if (!mockupContainer) return;
524
+
525
+ mockupContainer.addEventListener('click', (e) => {
526
+ // Support Shadow DOM event retargeting
527
+ const path = e.composedPath();
528
+ const target = path[0];
529
+
530
+ if (!target || target === mockupContainer || target === mockupContainer.shadowRoot) return;
531
+
532
+ e.preventDefault();
533
+ e.stopPropagation();
534
+
535
+ const nounDisplay = document.querySelector('#pp-object-inspector-content .qss-font-mono');
536
+ const metrics = document.querySelectorAll('#pp-object-inspector-content .qss-font-mono');
537
+
538
+ if (nounDisplay) {
539
+ nounDisplay.textContent = target.id ? `#${target.id}` : `<${target.tagName.toLowerCase()}>`;
540
+ }
541
+
542
+ const rect = target.getBoundingClientRect();
543
+ const parentRect = document.getElementById('qss-canvas').getBoundingClientRect();
544
+
545
+ // Quantize to 1000-unit grid
546
+ const qX = Math.round((rect.left - parentRect.left) / parentRect.width * 1000);
547
+ const qY = Math.round((rect.top - parentRect.top) / parentRect.height * 1000);
548
+ const qW = Math.round(rect.width / parentRect.width * 1000);
549
+ const qH = Math.round(rect.height / parentRect.height * 1000);
550
+
551
+ if (metrics[1]) metrics[1].textContent = qX;
552
+ if (metrics[2]) metrics[2].textContent = qY;
553
+ if (metrics[3]) metrics[3].textContent = qW;
554
+ if (metrics[4]) metrics[4].textContent = qH;
555
+
556
+ // Support Shadow DOM selection highlighting
557
+ const shadow = mockupContainer.shadowRoot;
558
+ if (shadow) {
559
+ shadow.querySelectorAll('.qss-inspector-selected').forEach(el => el.classList.remove('qss-inspector-selected'));
560
+ }
561
+ document.querySelectorAll('.qss-inspector-selected').forEach(el => el.classList.remove('qss-inspector-selected'));
562
+
563
+ target.classList.add('qss-inspector-selected');
564
+
565
+ // Refresh anchors if active
566
+ const anchorBtn = document.getElementById('pp-toggle-anchors');
567
+ if (anchorBtn && anchorBtn.classList.contains('bg-indigo-600')) {
568
+ showAnchors();
569
+ }
570
+
571
+ // Update Active Adjectives List (Chemistry)
572
+ const adjList = document.getElementById('pp-active-adjectives-list');
573
+ if (adjList) {
574
+ const adjectives = Array.from(target.classList).filter(c => c !== 'qss-inspector-selected');
575
+ if (adjectives.length === 0) {
576
+ adjList.innerHTML = '<div class="qss px[6] py[2] bg-slate-900 text-slate-500 rounded[2] text-size[8] italic">No adjectives</div>';
577
+ } else {
578
+ adjList.innerHTML = adjectives.sort().map(adj => `
579
+ <div class="qss horizontal v-mid gap[4] px[6] py[2] bg-slate-900 border[1] border-slate-800 rounded[2] text-size[8] text-slate-300 group">
580
+ <span class="qss font-mono">${adj}</span>
581
+ <button class="qss text-slate-600 hover:text-red-400 border-none bg-transparent clickable px[2]" data-action="remove-adj" data-adj="${adj}">&times;</button>
582
+ </div>
583
+ `).join('');
584
+
585
+ adjList.querySelectorAll('[data-action="remove-adj"]').forEach(btn => {
586
+ btn.addEventListener('click', (e) => {
587
+ e.stopPropagation();
588
+ const adj = btn.dataset.adj;
589
+ target.classList.remove(adj);
590
+ btn.parentElement.remove();
591
+ // Re-click to refresh metrics and anchors
592
+ target.click();
593
+ });
594
+ });
595
+ }
596
+ }
597
+
598
+ // Dispatch event for Explorer bar syncing
599
+ if (target.id) {
600
+ window.dispatchEvent(new CustomEvent('parapet:objectSelected', {
601
+ detail: { id: target.id, adjectives: Array.from(target.classList) }
602
+ }));
603
+ }
604
+ });
605
+ }
606
+
607
+ // Lifecycle Hardening: formal cleanup routine
608
+ function onDestroy() {
609
+ console.log("Styles Workbench: [Cleanup] Destroying components and removing listeners...");
610
+ if (monacoSource) { monacoSource.dispose(); monacoSource = null; }
611
+ if (monacoCompiled) { monacoCompiled.dispose(); monacoCompiled = null; }
612
+
613
+ document.removeEventListener('parapet:fileSelected', window._qssFileSelectedListener);
614
+ window.removeEventListener('resize', window._qssAuditResizeListener);
615
+ document.removeEventListener('workbench:refresh', window._qssAuditRefreshListener);
616
+
617
+ const workbench = document.getElementById('pp-styles-workbench');
618
+ if (workbench) delete workbench.dataset.initialized;
619
+ }
620
+
621
+ // Expose for Parapet Engine
622
+ window.ParapetWorkbench = { onDestroy };
623
+
624
+ function initConsoleResizer() {
625
+ const resizer = document.getElementById('pp-console-resizer');
626
+ const consoleEl = document.getElementById('pp-workspace-console');
627
+ const trigger = document.querySelector('.qss-console-trigger');
628
+ const chevron = document.querySelector('.qss-console-chevron');
629
+
630
+ if (!resizer || !consoleEl) return;
631
+
632
+ let isResizing = false;
633
+ let lastHeight = parseInt(localStorage.getItem('qss-console-height')) || 150;
634
+
635
+ if (trigger) {
636
+ trigger.addEventListener('click', () => {
637
+ const isCollapsed = consoleEl.offsetHeight <= 24;
638
+ if (isCollapsed) {
639
+ consoleEl.style.height = `${lastHeight}px`;
640
+ if (chevron) chevron.style.transform = 'rotate(180deg)';
641
+ } else {
642
+ lastHeight = consoleEl.offsetHeight;
643
+ localStorage.setItem('qss-console-height', lastHeight);
644
+ consoleEl.style.height = '24px';
645
+ if (chevron) chevron.style.transform = 'rotate(0deg)';
646
+ }
647
+ });
648
+ }
649
+
650
+ resizer.addEventListener('mousedown', (e) => {
651
+ isResizing = true;
652
+ document.addEventListener('mousemove', handleMouseMove);
653
+ document.addEventListener('mouseup', stopResizing);
654
+ document.body.style.cursor = 'row-resize';
655
+ e.preventDefault();
656
+ });
657
+
658
+ function handleMouseMove(e) {
659
+ if (!isResizing) return;
660
+ // 24 is the footer height
661
+ const newHeight = window.innerHeight - e.clientY - 24;
662
+ if (newHeight > 24 && newHeight < (window.innerHeight * 0.7)) {
663
+ consoleEl.style.height = `${newHeight}px`;
664
+ lastHeight = newHeight;
665
+ if (chevron) chevron.style.transform = 'rotate(180deg)';
666
+ }
667
+ }
668
+
669
+ function stopResizing() {
670
+ if (!isResizing) return;
671
+ isResizing = false;
672
+ localStorage.setItem('qss-console-height', lastHeight);
673
+ document.removeEventListener('mousemove', handleMouseMove);
674
+ document.removeEventListener('mouseup', stopResizing);
675
+ document.body.style.cursor = 'default';
676
+ }
677
+ }
678
+
679
+ function initFlushLogic() {
680
+ const flushBtn = document.getElementById('pp-commit-styles');
681
+ if (!flushBtn) return;
682
+
683
+ flushBtn.addEventListener('click', async () => {
684
+ if (!currentPath || !monacoSource) {
685
+ alert("No file loaded or editor not initialized.");
686
+ return;
687
+ }
688
+
689
+ flushBtn.disabled = true;
690
+ flushBtn.textContent = "COMMITTING...";
691
+
692
+ try {
693
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
694
+ const response = await fetch('/css_suite/flush', {
695
+ method: 'POST',
696
+ headers: {
697
+ 'Content-Type': 'application/json',
698
+ 'X-CSRF-Token': csrfToken
699
+ },
700
+ body: JSON.stringify({
701
+ path: currentPath,
702
+ content: monacoSource.getValue()
703
+ })
704
+ });
705
+
706
+ const data = await response.json();
707
+ if (data.success) {
708
+ console.info("Workbench: Successfully flushed changes to", currentPath);
709
+ flushBtn.textContent = "FLUSHED!";
710
+ setTimeout(() => {
711
+ flushBtn.textContent = "Commit Spatial Changes";
712
+ flushBtn.disabled = false;
713
+ }, 2000);
714
+ } else {
715
+ throw new Error(data.error || "Unknown error");
716
+ }
717
+ } catch (err) {
718
+ console.error("Workbench: Flush failed", err);
719
+ alert(`Flush failed: ${err.message}`);
720
+ flushBtn.textContent = "RETRY COMMIT";
721
+ flushBtn.disabled = false;
722
+ }
723
+ });
724
+ }
725
+
726
+ async function initGrammarPalette() {
727
+ const container = document.querySelector('#pp-object-inspector-content .qss-h\\[150\\]');
728
+ const searchInput = document.querySelector('#pp-object-inspector-content input[placeholder="Search adjectives..."]');
729
+ if (!container || !searchInput) return;
730
+
731
+ try {
732
+ const response = await fetch('/css_suite/grammar');
733
+ const registry = await response.json();
734
+
735
+ function renderRules(filter = '') {
736
+ let html = '';
737
+ registry.categories.forEach(cat => {
738
+ const filteredRules = cat.rules.filter(r =>
739
+ r.selector.toLowerCase().includes(filter.toLowerCase()) ||
740
+ r.description.toLowerCase().includes(filter.toLowerCase())
741
+ );
742
+
743
+ if (filteredRules.length > 0) {
744
+ html += `<div class="qss-text-size[8] qss-text-slate-600 qss-uppercase qss-font-bold qss-px[4] qss-mt[8]">${cat.name}</div>`;
745
+ filteredRules.forEach(rule => {
746
+ html += `
747
+ <div class="qss-p[6] qss-rounded hover:qss-bg-white/5 qss-cursor-pointer qss-text-size[10] qss-text-slate-300 qss-group qss-relative"
748
+ data-selector="${rule.selector}" data-param="${rule.parameter}">
749
+ <span class="qss-font-mono">${rule.selector}</span>
750
+ <div class="qss-text-size[8] qss-text-slate-500 qss-hidden group-hover:qss-block">${rule.description}</div>
751
+ </div>
752
+ `;
753
+ });
754
+ }
755
+ });
756
+ container.innerHTML = html || '<div class="qss-p[12] qss-text-size[9] qss-text-slate-600 qss-italic">No matches found</div>';
757
+ }
758
+
759
+ searchInput.addEventListener('input', (e) => renderRules(e.target.value));
760
+
761
+ container.addEventListener('click', (e) => {
762
+ const item = e.target.closest('[data-selector]');
763
+ if (!item) return;
764
+
765
+ const selector = item.dataset.selector;
766
+ const paramType = item.dataset.param;
767
+ const selectedEl = document.querySelector('.qss-inspector-selected');
768
+
769
+ if (!selectedEl) {
770
+ console.warn("Grammar Palette: No element selected in canvas.");
771
+ return;
772
+ }
773
+
774
+ if (paramType === 'number') {
775
+ const val = prompt(`Enter value for ${selector}:`);
776
+ if (val !== null) {
777
+ const finalClass = selector.replace('...', val);
778
+ applyClassToElement(selectedEl, finalClass);
779
+ }
780
+ } else if (paramType === 'literal') {
781
+ // Extract literal from selector e.g. .qss-depth[canvas] -> canvas
782
+ const match = selector.match(/\[(.*?)\]/);
783
+ if (match) {
784
+ applyClassToElement(selectedEl, selector.replace('.', ''));
785
+ }
786
+ } else {
787
+ applyClassToElement(selectedEl, selector.replace('.', ''));
788
+ }
789
+ });
790
+
791
+ renderRules();
792
+
793
+ } catch (err) {
794
+ console.error("Workbench: Failed to load grammar registry", err);
795
+ }
796
+ }
797
+
798
+ function applyClassToElement(el, className) {
799
+ // Strip leading dots — use Naked Adjectives (v0.5.0 protocol)
800
+ let cleanClass = className.replace(/^\./, '');
801
+
802
+ // Logical mutual exclusivity (e.g. can't have two pos-x classes)
803
+ const prefix = cleanClass.split('[')[0];
804
+ Array.from(el.classList).forEach(c => {
805
+ if (c.startsWith(prefix) && c !== cleanClass) {
806
+ el.classList.remove(c);
807
+ }
808
+ });
809
+
810
+ el.classList.add(cleanClass);
811
+ console.log(`Workbench: Applied ${cleanClass} to ${el.id || el.tagName}`);
812
+
813
+ // Trigger a refresh of the Object Inspector metrics
814
+ el.click();
815
+ }
816
+
817
+ document.addEventListener('DOMContentLoaded', init);
818
+ document.addEventListener('turbo:load', init);
819
+ })();