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,439 @@
1
+ (function(global) {
2
+
3
+ class ActivityCard {
4
+ constructor(options = {}) {
5
+ this.id = options.id || `card-${Math.random().toString(36).substr(2, 9)}`;
6
+
7
+ // Subclasses declare identity via getters (cardTitle, cardIcon, cardKeyArea, cardProficiency).
8
+ // options can override at spawn time.
9
+ let icon = this.cardIcon || options.icon || 'layout';
10
+ if (!icon.includes('<svg') && !icon.includes('<i')) {
11
+ icon = `<i class="lucide lucide-${icon} qss-w[16] qss-h[16] qss-opacity-100"></i>`;
12
+ }
13
+ this.title = this.cardTitle || options.title || 'Untitled Activity';
14
+ this.icon = icon;
15
+ this.keyArea = this.cardKeyArea || options.keyArea || 'general';
16
+ this.proficiency = this.cardProficiency !== undefined ? this.cardProficiency : (options.proficiency || 0);
17
+
18
+ this.element = null;
19
+ this.isDragging = false;
20
+ this.isResizing = false;
21
+
22
+ // Merge: base defaults → subclass defaults() → spawn-time options
23
+ const initialProps = { ...this.defaults(), ...options };
24
+ const savedState = this.loadState();
25
+
26
+ // Merge saved state over defaults to ensure new properties are present
27
+ this.state = savedState ? { ...initialProps, ...savedState } : initialProps;
28
+
29
+ // Initialize preferred dimensions for expansion logic
30
+ this.state.preferredWidth = this.state.preferredWidth || this.state.width;
31
+ this.state.preferredHeight = this.state.preferredHeight || this.state.height;
32
+
33
+ if (!ActivityCards.Z_LEVELS.includes(this.state.zIndex)) {
34
+ this.state.zIndex = this.getBaseZ();
35
+ }
36
+
37
+ // Subclass provides content via buildContent()
38
+ this.contentHtml = this.buildContent();
39
+ }
40
+
41
+ // ── Subclass overrides ────────────────────────────────────────────────
42
+
43
+ // Static identity — override as getters in subclass
44
+ get cardTitle() { return null; }
45
+ get cardIcon() { return null; }
46
+ get cardKeyArea() { return null; }
47
+ get cardProficiency() { return undefined; }
48
+
49
+ // Default state shape — subclass spreads and overrides
50
+ defaults() {
51
+ return {
52
+ width: 400, height: 300, x: 20, y: 20, zIndex: 10,
53
+ pinned: false, active: true, orientation: 'neutral', size: 'medium',
54
+ minWidth: 150, minHeight: 100, userPositioned: false
55
+ };
56
+ }
57
+
58
+ // Card body HTML — subclass returns its own markup
59
+ buildContent() { return this._getFallbackContent(); }
60
+
61
+ // Lifecycle hooks — subclass overrides as needed
62
+ onInit() { }
63
+ onResize() { }
64
+ onDestroy() { }
65
+
66
+ // Card header actions — subclass returns HTML for additional buttons
67
+ renderHeaderActions() { return ''; }
68
+
69
+ // ── Internal ──────────────────────────────────────────────────────────
70
+
71
+ _getFallbackContent() {
72
+ const template = document.getElementById(`qss-tpl-${this.id}`);
73
+ if (template) return template.innerHTML;
74
+ return `
75
+ <div class="qss-flex qss-flex-col qss-gap[8] qss-p[16] qss-text-size[11] qss-font-mono qss-text-slate-500">
76
+ <div class="qss-flex qss-justify-between qss-border-b qss-border-slate-800/50 qss-pb[4]"><span>ID:</span><span class="qss-text-slate-300">${this.id}</span></div>
77
+ <div class="qss-flex qss-justify-between qss-border-b qss-border-slate-800/50 qss-pb[4]"><span>Proficiency:</span><span class="qss-text-slate-300">${this.proficiency}</span></div>
78
+ <div class="qss-flex qss-justify-between qss-border-b qss-border-slate-800/50 qss-pb[4]"><span>Key Area:</span><span class="qss-text-slate-300">${this.keyArea}</span></div>
79
+ <div class="qss-flex qss-justify-between qss-border-b qss-border-slate-800/50 qss-pb[4]"><span>Z-Index:</span><span class="qss-text-slate-300">${this.state.zIndex}</span></div>
80
+ </div>
81
+ `;
82
+ }
83
+
84
+ render(container) {
85
+ this.container = container;
86
+ const card = document.createElement('div');
87
+ card.id = this.id;
88
+ card.className = `qss-activity-card qss-proficiency-border-${this.proficiency} ${this.state.pinned ? 'qss-pinned' : ''}`;
89
+ card.setAttribute('data-key-area', this.keyArea);
90
+ card.setAttribute('data-proficiency', this.proficiency);
91
+ card.setAttribute('data-orientation', this.state.orientation);
92
+ card.setAttribute('data-size', this.state.size);
93
+
94
+ // The coordinates (x,y) and dimensions (width,height) refer to the static Bounding Box.
95
+ card.style.width = `${this.state.width}px`;
96
+ card.style.height = `${this.state.height}px`;
97
+ card.style.zIndex = this.state.zIndex;
98
+ card.classList.add('qss-absolute');
99
+ card.style.left = `${this.state.x}px`;
100
+ card.style.top = `${this.state.y}px`;
101
+
102
+ card.innerHTML = `
103
+ <div class="qss-card-inner-container">
104
+ <div class="qss-card-header qss-px[6]">
105
+ <div class="qss-flex qss-items-center qss-gap[6] qss-overflow-hidden">
106
+ <div class="qss-flex qss-items-center qss-justify-center qss-w[16] qss-h[16] qss-text-slate-300 qss-activity-card-icon">
107
+ ${this.icon}
108
+ </div>
109
+ <span class="qss-card-title qss-truncate">${this.title}</span>
110
+ </div>
111
+ <div class="qss-flex qss-items-center qss-gap[8] qss-flex-shrink-0">
112
+ <div class="qss-header-actions qss-flex qss-items-center qss-gap[8]">
113
+ ${this.renderHeaderActions()}
114
+ </div>
115
+ <div class="qss-flex qss-items-center qss-bg-slate-900/50 qss-rounded qss-px[4] qss-py[2] qss-gap[6] qss-border qss-border-slate-800/50">
116
+ <div class="qss-z-down-btn qss-cursor-pointer qss-text-slate-500 hover:qss-text-white qss-flex qss-items-center qss-justify-center qss-w[10] qss-h[10] qss-transition-colors">
117
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" style="width:100%;height:100%;display:block;"><polyline points="6 9 12 15 18 9"></polyline></svg>
118
+ </div>
119
+ <span class="qss-text-size[9] qss-font-bold qss-text-slate-300 qss-min-w-[12px] qss-text-center qss-z-index-display qss-select-none">${ActivityCards.zLabel(this.state.zIndex)}</span>
120
+ <div class="qss-z-up-btn qss-cursor-pointer qss-text-slate-500 hover:qss-text-white qss-flex qss-items-center qss-justify-center qss-w[10] qss-h[10] qss-transition-colors">
121
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" style="width:100%;height:100%;display:block;"><polyline points="18 15 12 9 6 15"></polyline></svg>
122
+ </div>
123
+ </div>
124
+ <div class="qss-pin-btn qss-cursor-pointer qss-text-slate-500 hover:qss-text-indigo-400 qss-flex qss-items-center qss-justify-center qss-w[12] qss-h[12] qss-transition-colors ${this.state.pinned ? 'qss-pin-active' : ''}">
125
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width:100%;height:100%;display:block;">
126
+ <line x1="12" y1="17" x2="12" y2="22"></line>
127
+ <path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"></path>
128
+ </svg>
129
+ </div>
130
+ <div class="qss-w[1] qss-h[12] qss-bg-slate-800/50"></div>
131
+ <div class="qss-close-btn qss-cursor-pointer qss-text-slate-500 hover:qss-text-red-400 qss-items-center qss-justify-center qss-w[12] qss-h[12] qss-transition-colors ${this.id === 'file-editor' && ActivityCards.getProficiencyLevel() < 2 ? 'qss-hidden' : 'qss-flex'}">
132
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="width:100%;height:100%;display:block;">
133
+ <line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>
134
+ </svg>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ <div class="qss-card-content qss-custom-scrollbar">${this.contentHtml}</div>
139
+ <div class="qss-resize-handle qss-absolute qss-bottom-1 qss-right-1 qss-w[16] qss-h[16] qss-cursor-se-resize qss-flex qss-items-end qss-justify-end qss-p[2] qss-opacity-30 hover:qss-opacity-100">
140
+ <svg width="8" height="8" viewBox="0 0 8 8" fill="none"><path d="M7 1V7H1" stroke="currentColor" stroke-width="1" stroke-linecap="round"/></svg>
141
+ </div>
142
+ </div>
143
+ `;
144
+
145
+ this.element = card;
146
+ const targetLayer = this.getTargetLayer(container);
147
+ targetLayer.appendChild(card);
148
+ this.constrainToBoundaries();
149
+ this.alignToGrid();
150
+ this.updateElementStyles();
151
+ this.initInteractions();
152
+ this.onInit();
153
+ return card;
154
+ }
155
+
156
+ getTargetLayer(container) {
157
+ if (!this.state.pinned) return container.querySelector('#pp-floating-layer') || container;
158
+ return container.querySelector(`[data-layer="${this.proficiency}"]`) || container;
159
+ }
160
+
161
+ initInteractions() {
162
+ const header = this.element.querySelector('.qss-card-header');
163
+ const resizeHandle = this.element.querySelector('.qss-resize-handle');
164
+ const closeBtn = this.element.querySelector('.qss-close-btn');
165
+ const pinBtn = this.element.querySelector('.qss-pin-btn');
166
+ const zUpBtn = this.element.querySelector('.qss-z-up-btn');
167
+ const zDownBtn = this.element.querySelector('.qss-z-down-btn');
168
+ const zDisplay = this.element.querySelector('.qss-z-index-display');
169
+
170
+ zUpBtn.addEventListener('click', (e) => {
171
+ e.stopPropagation();
172
+ const levels = ActivityCards.Z_LEVELS;
173
+ const idx = levels.indexOf(this.state.zIndex);
174
+ const nextIdx = Math.min(idx >= 0 ? idx + 1 : 0, levels.length - 1);
175
+ this.state.zIndex = levels[nextIdx];
176
+ this.element.classList.toggle('qss-promoted', this.state.zIndex === ActivityCards.PROMOTE_Z);
177
+ this.updateZIndexUI(zDisplay);
178
+ });
179
+ zDownBtn.addEventListener('click', (e) => {
180
+ e.stopPropagation();
181
+ const levels = ActivityCards.Z_LEVELS;
182
+ const idx = levels.indexOf(this.state.zIndex);
183
+ if (idx <= 0) return;
184
+ this.state.zIndex = levels[idx - 1];
185
+ this.element.classList.remove('qss-promoted');
186
+ if (ActivityCards.state.promotedCardId === this.id) ActivityCards.state.promotedCardId = null;
187
+ this.updateZIndexUI(zDisplay);
188
+ });
189
+ pinBtn.addEventListener('click', (e) => { e.stopPropagation(); this.togglePin(); });
190
+ header.addEventListener('mousedown', (e) => this.startDrag(e));
191
+ resizeHandle.addEventListener('mousedown', (e) => this.startResize(e));
192
+ closeBtn.addEventListener('click', () => this.destroy());
193
+
194
+ document.addEventListener('mousemove', (e) => {
195
+ if (this.isDragging) { e.preventDefault(); this.drag(e); }
196
+ if (this.isResizing) { e.preventDefault(); this.resize(e); }
197
+ });
198
+
199
+ document.addEventListener('mouseup', (e) => {
200
+ if (this.isDragging || this.isResizing) {
201
+ const wasDragging = this.isDragging;
202
+ const wasResizing = this.isResizing;
203
+ this.isDragging = false;
204
+ this.isResizing = false;
205
+ this.element.classList.remove('qss-dragging');
206
+ document.body.style.userSelect = '';
207
+ this.alignToGrid();
208
+ if (e.ctrlKey) this.applyMagneticSnapping();
209
+ if (wasDragging || wasResizing) {
210
+ this.state.userPositioned = true;
211
+ this.handleCollisions();
212
+ const restoreZ = this._preDragZIndex !== undefined ? this._preDragZIndex : this.getBaseZ();
213
+ this.state.zIndex = restoreZ;
214
+ this.element.style.zIndex = restoreZ;
215
+ const zDisplay = this.element.querySelector('.qss-z-index-display');
216
+ if (zDisplay) zDisplay.textContent = ActivityCards.zLabel(restoreZ);
217
+ this._preDragZIndex = undefined;
218
+ }
219
+ this.saveState();
220
+ this.updateElementStyles();
221
+ }
222
+ });
223
+ }
224
+
225
+ alignToGrid() {
226
+ const G = ActivityCards.settings.gridSize;
227
+ this.state.x = Math.round(this.state.x / G) * G;
228
+ this.state.y = Math.round(this.state.y / G) * G;
229
+ this.state.width = Math.round(this.state.width / G) * G;
230
+ this.state.height = Math.round(this.state.height / G) * G;
231
+ }
232
+
233
+ applyMagneticSnapping() {
234
+ const G = ActivityCards.settings.gridSize;
235
+ const THRESHOLD = G * 2;
236
+ const MARGIN = G;
237
+ const peers = Object.values(ActivityCards.active).filter(c => c !== this && c.state.zIndex === this.state.zIndex);
238
+ peers.forEach(t => {
239
+ const tr = { left: t.state.x, right: t.state.x + t.state.width, top: t.state.y, bottom: t.state.y + t.state.height };
240
+ const sr = { left: this.state.x, right: this.state.x + this.state.width, top: this.state.y, bottom: this.state.y + this.state.height };
241
+ if (Math.abs(sr.left - (tr.right + MARGIN)) < THRESHOLD) this.state.x = tr.right + MARGIN;
242
+ else if (Math.abs(sr.right - (tr.left - MARGIN)) < THRESHOLD) this.state.x = tr.left - MARGIN - this.state.width;
243
+ if (Math.abs(sr.top - (tr.bottom + MARGIN)) < THRESHOLD) this.state.y = tr.bottom + MARGIN;
244
+ else if (Math.abs(sr.bottom - (tr.top - MARGIN)) < THRESHOLD) this.state.y = tr.top - MARGIN - this.state.height;
245
+ });
246
+ if (this.container) {
247
+ const cRect = this.container.getBoundingClientRect();
248
+ if (Math.abs(this.state.x) < THRESHOLD) this.state.x = 0;
249
+ if (Math.abs(this.state.y) < THRESHOLD) this.state.y = 0;
250
+ if (Math.abs(this.state.x + this.state.width - cRect.width) < THRESHOLD) this.state.x = cRect.width - this.state.width;
251
+ if (Math.abs(this.state.y + this.state.height - cRect.height) < THRESHOLD) this.state.y = cRect.height - this.state.height;
252
+ }
253
+ }
254
+
255
+ handleCollisions() {
256
+ const peers = Object.values(ActivityCards.active).filter(c => c !== this && c.state.zIndex === this.state.zIndex);
257
+ let found = true, safety = 0;
258
+ while (found && safety < 10) {
259
+ found = false;
260
+ for (const t of peers) {
261
+ if (this.isOverlapping(this.state, t.state)) { found = true; this.nudgeCard(t); break; }
262
+ }
263
+ safety++;
264
+ }
265
+ }
266
+
267
+ isOverlapping(r1, r2) { return !(r1.x + r1.width <= r2.x || r2.x + r2.width <= r1.x || r1.y + r1.height <= r2.y || r2.y + r2.height <= r1.y); }
268
+
269
+ nudgeCard(target) {
270
+ target.state.x += ActivityCards.settings.gridSize;
271
+ target.alignToGrid();
272
+ target.constrainToBoundaries();
273
+ target.updateElementStyles();
274
+ target.saveState();
275
+ }
276
+
277
+ updateElementStyles() {
278
+ if (!this.element) return;
279
+ this.element.style.width = `${this.state.width}px`;
280
+ this.element.style.height = `${this.state.height}px`;
281
+ if (!this.state.pinned) {
282
+ this.element.style.left = `${this.state.x}px`;
283
+ this.element.style.top = `${this.state.y}px`;
284
+ }
285
+ }
286
+
287
+ updateZIndexUI(displayElement) {
288
+ this.element.style.zIndex = this.state.zIndex;
289
+ if (displayElement) displayElement.textContent = ActivityCards.zLabel(this.state.zIndex);
290
+ this.saveState();
291
+ }
292
+
293
+ getBaseZ() {
294
+ const levels = ActivityCards.Z_LEVELS;
295
+ return levels[Math.min(this.proficiency, levels.length - 2)];
296
+ }
297
+
298
+ demote() {
299
+ const base = this.getBaseZ();
300
+ this.state.zIndex = base;
301
+ this.element.style.zIndex = base;
302
+ this.element.classList.remove('qss-promoted');
303
+ const zDisplay = this.element.querySelector('.qss-z-index-display');
304
+ if (zDisplay) zDisplay.textContent = ActivityCards.zLabel(base);
305
+ if (ActivityCards.state.promotedCardId === this.id) ActivityCards.state.promotedCardId = null;
306
+ this.saveState();
307
+ }
308
+
309
+ togglePin() {
310
+ this.state.pinned = !this.state.pinned;
311
+ const pinBtn = this.element.querySelector('.qss-pin-btn');
312
+ if (this.state.pinned) { this.element.classList.add('qss-pinned'); pinBtn.classList.add('qss-pin-active'); }
313
+ else { this.element.classList.remove('qss-pinned'); pinBtn.classList.remove('qss-pin-active'); }
314
+ if (this.container) { this.getTargetLayer(this.container).appendChild(this.element); }
315
+ this.saveState();
316
+ }
317
+
318
+ startDrag(e) {
319
+ if (this.state.pinned) return;
320
+ if (e.target.closest('.qss-card-header')) {
321
+ this.isDragging = true;
322
+ this.dragStartX = e.clientX - this.element.offsetLeft;
323
+ this.dragStartY = e.clientY - this.element.offsetTop;
324
+ this._preDragZIndex = this.state.zIndex;
325
+ ActivityCards.state.lastFocused = this.id;
326
+ this.element.style.zIndex = ActivityCards.DRAG_Z;
327
+ this.element.classList.add('qss-dragging');
328
+ document.body.style.userSelect = 'none';
329
+ }
330
+ }
331
+
332
+ drag(e) {
333
+ if (!this.isDragging || this.state.pinned) return;
334
+ this.state.x = e.clientX - this.dragStartX;
335
+ this.state.y = e.clientY - this.dragStartY;
336
+ this.constrainToBoundaries();
337
+ this.element.style.left = `${this.state.x}px`;
338
+ this.element.style.top = `${this.state.y}px`;
339
+ }
340
+
341
+ constrainToBoundaries(targetW, targetH) {
342
+ if (!this.container && targetW === undefined) return;
343
+
344
+ // Use passed dimensions (from ResizeObserver) or measure container
345
+ const widthBound = targetW !== undefined ? targetW : this.container.getBoundingClientRect().width;
346
+ const heightBound = targetH !== undefined ? targetH : this.container.getBoundingClientRect().height;
347
+
348
+ // Expansion Logic: Try to grow back to preferred size if space was previously restricted
349
+ if (this.state.width < this.state.preferredWidth && this.state.width < widthBound) {
350
+ this.state.width = Math.min(this.state.preferredWidth, widthBound - this.state.x);
351
+ }
352
+ if (this.state.height < this.state.preferredHeight && this.state.height < heightBound) {
353
+ this.state.height = Math.min(this.state.preferredHeight, heightBound - this.state.y);
354
+ }
355
+
356
+ // Shrink Logic: Force shrink if card is now larger than workspace
357
+ if (this.state.width > widthBound) {
358
+ this.state.width = Math.max(this.state.minWidth, widthBound);
359
+ }
360
+ if (this.state.height > heightBound) {
361
+ this.state.height = Math.max(this.state.minHeight, heightBound);
362
+ }
363
+
364
+ // Reposition Logic: Push card back into view if it's hanging off right/bottom
365
+ this.state.x = Math.max(0, Math.min(this.state.x, widthBound - this.state.width));
366
+ this.state.y = Math.max(0, Math.min(this.state.y, heightBound - this.state.height));
367
+
368
+ // Final safety clamp
369
+ if (this.state.x + this.state.width > widthBound) {
370
+ this.state.width = Math.max(this.state.minWidth, widthBound - this.state.x);
371
+ }
372
+ if (this.state.y + this.state.height > heightBound) {
373
+ this.state.height = Math.max(this.state.minHeight, heightBound - this.state.y);
374
+ }
375
+ }
376
+
377
+ startResize(e) {
378
+ if (this.state.pinned) return;
379
+ this.isResizing = true;
380
+ this.resizeStartX = e.clientX;
381
+ this.resizeStartY = e.clientY;
382
+ this.resizeStartWidth = this.element.offsetWidth;
383
+ this.resizeStartHeight = this.element.offsetHeight;
384
+ e.stopPropagation();
385
+ e.preventDefault();
386
+ }
387
+
388
+ resize(e) {
389
+ if (!this.isResizing) return;
390
+ let newW = this.resizeStartWidth + (e.clientX - this.resizeStartX);
391
+ let newH = this.resizeStartHeight + (e.clientY - this.resizeStartY);
392
+ newW = Math.max(this.state.minWidth, newW);
393
+ newH = Math.max(this.state.minHeight, newH);
394
+ if (this.state.maxWidth) newW = Math.min(this.state.maxWidth, newW);
395
+ if (this.state.maxHeight) newH = Math.min(this.state.maxHeight, newH);
396
+ if (this.container) {
397
+ const cRect = this.container.getBoundingClientRect();
398
+ newW = Math.min(newW, cRect.width - this.state.x);
399
+ newH = Math.min(newH, cRect.height - this.state.y);
400
+ }
401
+ this.state.width = newW;
402
+ this.state.height = newH;
403
+
404
+ // Manual resize updates preferred dimensions
405
+ this.state.preferredWidth = newW;
406
+ this.state.preferredHeight = newH;
407
+
408
+ this.element.style.width = `${this.state.width}px`;
409
+ this.element.style.height = `${this.state.height}px`;
410
+ this.onResize();
411
+ }
412
+
413
+ saveState() { ActivityCards.updateCardState(this.id, this.state); }
414
+ loadState() { return ActivityCards.getCardState(this.id); }
415
+
416
+ destroy() {
417
+ // Enforcement: File Editor cannot be closed at Novice/Intermediate levels
418
+ if (this.id === 'file-editor') {
419
+ const level = ActivityCards.getProficiencyLevel();
420
+ if (level !== -1 && level < 2) {
421
+ ActivityCards.showNotice("File Editor is required at this level", "error");
422
+ return;
423
+ }
424
+ }
425
+
426
+ this.onDestroy();
427
+ if (this.element) this.element.remove();
428
+ if (ActivityCards.active[this.id]) {
429
+ delete ActivityCards.active[this.id];
430
+ this.state.visible = false;
431
+ ActivityCards.updateCardState(this.id, this.state);
432
+ }
433
+ document.dispatchEvent(new CustomEvent('cardClosed', { detail: { id: this.id } }));
434
+ }
435
+ }
436
+
437
+ global.ActivityCard = ActivityCard;
438
+
439
+ })(typeof window !== 'undefined' ? window : this);