not_pressed-core 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 (157) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +41 -0
  3. data/README.md +285 -0
  4. data/app/assets/javascripts/not_pressed/lightbox.js +110 -0
  5. data/app/assets/stylesheets/not_pressed/admin.css +1161 -0
  6. data/app/assets/stylesheets/not_pressed/content.css +193 -0
  7. data/app/assets/stylesheets/not_pressed/gallery.css +117 -0
  8. data/app/assets/stylesheets/not_pressed/themes/starter.css +118 -0
  9. data/app/controllers/not_pressed/admin/base_controller.rb +21 -0
  10. data/app/controllers/not_pressed/admin/categories_controller.rb +53 -0
  11. data/app/controllers/not_pressed/admin/content_blocks_controller.rb +73 -0
  12. data/app/controllers/not_pressed/admin/dashboard_controller.rb +19 -0
  13. data/app/controllers/not_pressed/admin/forms_controller.rb +86 -0
  14. data/app/controllers/not_pressed/admin/media_attachments_controller.rb +94 -0
  15. data/app/controllers/not_pressed/admin/pages_controller.rb +122 -0
  16. data/app/controllers/not_pressed/admin/plugins_controller.rb +121 -0
  17. data/app/controllers/not_pressed/admin/settings_controller.rb +19 -0
  18. data/app/controllers/not_pressed/admin/tags_controller.rb +37 -0
  19. data/app/controllers/not_pressed/admin/themes_controller.rb +104 -0
  20. data/app/controllers/not_pressed/application_controller.rb +6 -0
  21. data/app/controllers/not_pressed/blog_controller.rb +83 -0
  22. data/app/controllers/not_pressed/form_submissions_controller.rb +70 -0
  23. data/app/controllers/not_pressed/pages_controller.rb +36 -0
  24. data/app/controllers/not_pressed/robots_controller.rb +34 -0
  25. data/app/controllers/not_pressed/sitemaps_controller.rb +12 -0
  26. data/app/helpers/not_pressed/admin_helper.rb +41 -0
  27. data/app/helpers/not_pressed/application_helper.rb +6 -0
  28. data/app/helpers/not_pressed/code_injection_helper.rb +29 -0
  29. data/app/helpers/not_pressed/content_helper.rb +13 -0
  30. data/app/helpers/not_pressed/form_helper.rb +80 -0
  31. data/app/helpers/not_pressed/media_helper.rb +28 -0
  32. data/app/helpers/not_pressed/seo_helper.rb +69 -0
  33. data/app/helpers/not_pressed/theme_helper.rb +42 -0
  34. data/app/mailers/not_pressed/application_mailer.rb +10 -0
  35. data/app/mailers/not_pressed/form_mailer.rb +15 -0
  36. data/app/models/concerns/not_pressed/sluggable.rb +43 -0
  37. data/app/models/not_pressed/category.rb +16 -0
  38. data/app/models/not_pressed/content_block.rb +46 -0
  39. data/app/models/not_pressed/form.rb +55 -0
  40. data/app/models/not_pressed/form_field.rb +23 -0
  41. data/app/models/not_pressed/form_submission.rb +19 -0
  42. data/app/models/not_pressed/media_attachment.rb +68 -0
  43. data/app/models/not_pressed/page.rb +182 -0
  44. data/app/models/not_pressed/page_version.rb +15 -0
  45. data/app/models/not_pressed/setting.rb +20 -0
  46. data/app/models/not_pressed/tag.rb +15 -0
  47. data/app/models/not_pressed/tagging.rb +12 -0
  48. data/app/plugins/not_pressed/analytics_plugin.rb +106 -0
  49. data/app/plugins/not_pressed/callout_block_plugin.rb +43 -0
  50. data/app/themes/not_pressed/starter_theme.rb +26 -0
  51. data/app/views/layouts/not_pressed/admin.html.erb +745 -0
  52. data/app/views/layouts/not_pressed/application.html.erb +12 -0
  53. data/app/views/layouts/not_pressed/page.html.erb +22 -0
  54. data/app/views/not_pressed/admin/categories/index.html.erb +58 -0
  55. data/app/views/not_pressed/admin/content_blocks/_block.html.erb +18 -0
  56. data/app/views/not_pressed/admin/content_blocks/_block_picker.html.erb +32 -0
  57. data/app/views/not_pressed/admin/content_blocks/create.turbo_stream.erb +3 -0
  58. data/app/views/not_pressed/admin/content_blocks/destroy.turbo_stream.erb +1 -0
  59. data/app/views/not_pressed/admin/content_blocks/editors/_callout.html.erb +38 -0
  60. data/app/views/not_pressed/admin/content_blocks/editors/_code.html.erb +26 -0
  61. data/app/views/not_pressed/admin/content_blocks/editors/_divider.html.erb +4 -0
  62. data/app/views/not_pressed/admin/content_blocks/editors/_form.html.erb +16 -0
  63. data/app/views/not_pressed/admin/content_blocks/editors/_gallery.html.erb +75 -0
  64. data/app/views/not_pressed/admin/content_blocks/editors/_heading.html.erb +26 -0
  65. data/app/views/not_pressed/admin/content_blocks/editors/_html.html.erb +13 -0
  66. data/app/views/not_pressed/admin/content_blocks/editors/_image.html.erb +56 -0
  67. data/app/views/not_pressed/admin/content_blocks/editors/_quote.html.erb +24 -0
  68. data/app/views/not_pressed/admin/content_blocks/editors/_text.html.erb +28 -0
  69. data/app/views/not_pressed/admin/content_blocks/editors/_video.html.erb +25 -0
  70. data/app/views/not_pressed/admin/dashboard/index.html.erb +60 -0
  71. data/app/views/not_pressed/admin/forms/_field_row.html.erb +33 -0
  72. data/app/views/not_pressed/admin/forms/_form.html.erb +75 -0
  73. data/app/views/not_pressed/admin/forms/edit.html.erb +1 -0
  74. data/app/views/not_pressed/admin/forms/index.html.erb +32 -0
  75. data/app/views/not_pressed/admin/forms/new.html.erb +1 -0
  76. data/app/views/not_pressed/admin/forms/submissions.html.erb +34 -0
  77. data/app/views/not_pressed/admin/media_attachments/_media_card.html.erb +21 -0
  78. data/app/views/not_pressed/admin/media_attachments/_picker.html.erb +19 -0
  79. data/app/views/not_pressed/admin/media_attachments/edit.html.erb +57 -0
  80. data/app/views/not_pressed/admin/media_attachments/index.html.erb +48 -0
  81. data/app/views/not_pressed/admin/pages/_form.html.erb +177 -0
  82. data/app/views/not_pressed/admin/pages/_page_tree_node.html.erb +32 -0
  83. data/app/views/not_pressed/admin/pages/edit.html.erb +69 -0
  84. data/app/views/not_pressed/admin/pages/index.html.erb +21 -0
  85. data/app/views/not_pressed/admin/pages/new.html.erb +1 -0
  86. data/app/views/not_pressed/admin/pages/preview.html.erb +17 -0
  87. data/app/views/not_pressed/admin/plugins/_settings_form.html.erb +59 -0
  88. data/app/views/not_pressed/admin/plugins/index.html.erb +48 -0
  89. data/app/views/not_pressed/admin/plugins/show.html.erb +54 -0
  90. data/app/views/not_pressed/admin/settings/code_injection.html.erb +21 -0
  91. data/app/views/not_pressed/admin/shared/_breadcrumbs.html.erb +14 -0
  92. data/app/views/not_pressed/admin/shared/_flash.html.erb +7 -0
  93. data/app/views/not_pressed/admin/shared/_modal.html.erb +9 -0
  94. data/app/views/not_pressed/admin/shared/_sidebar.html.erb +18 -0
  95. data/app/views/not_pressed/admin/tags/index.html.erb +52 -0
  96. data/app/views/not_pressed/admin/themes/index.html.erb +60 -0
  97. data/app/views/not_pressed/admin/themes/show.html.erb +66 -0
  98. data/app/views/not_pressed/blog/_post_card.html.erb +24 -0
  99. data/app/views/not_pressed/blog/feed.rss.builder +22 -0
  100. data/app/views/not_pressed/blog/index.html.erb +56 -0
  101. data/app/views/not_pressed/blog/show.html.erb +41 -0
  102. data/app/views/not_pressed/form_mailer/submission_notification.text.erb +8 -0
  103. data/app/views/not_pressed/pages/show.html.erb +4 -0
  104. data/app/views/themes/starter/layouts/not_pressed/default.html.erb +36 -0
  105. data/app/views/themes/starter/layouts/not_pressed/full_width.html.erb +36 -0
  106. data/app/views/themes/starter/layouts/not_pressed/sidebar.html.erb +41 -0
  107. data/config/routes.rb +81 -0
  108. data/db/migrate/20260310000001_create_not_pressed_pages.rb +20 -0
  109. data/db/migrate/20260310000002_create_not_pressed_content_blocks.rb +17 -0
  110. data/db/migrate/20260310000003_create_not_pressed_media_attachments.rb +14 -0
  111. data/db/migrate/20260310000004_add_content_type_to_not_pressed_pages.rb +8 -0
  112. data/db/migrate/20260310000005_add_publishing_fields_to_not_pressed_pages.rb +8 -0
  113. data/db/migrate/20260310000006_create_not_pressed_page_versions.rb +16 -0
  114. data/db/migrate/20260310000007_add_settings_fields_to_not_pressed_pages.rb +11 -0
  115. data/db/migrate/20260311000001_create_not_pressed_forms.rb +42 -0
  116. data/db/migrate/20260311000002_add_seo_fields_to_not_pressed_pages.rb +10 -0
  117. data/db/migrate/20260311000003_add_code_injection_to_not_pressed_pages.rb +8 -0
  118. data/db/migrate/20260311000004_create_not_pressed_settings.rb +14 -0
  119. data/db/migrate/20260312000001_create_not_pressed_categories.rb +16 -0
  120. data/db/migrate/20260312000002_create_not_pressed_tags.rb +15 -0
  121. data/db/migrate/20260312000003_create_not_pressed_taggings.rb +14 -0
  122. data/db/migrate/20260312000004_add_category_id_to_not_pressed_pages.rb +7 -0
  123. data/lib/generators/not_pressed/install/install_generator.rb +52 -0
  124. data/lib/generators/not_pressed/install/templates/initializer.rb.tt +89 -0
  125. data/lib/generators/not_pressed/install/templates/seeds.rb.tt +131 -0
  126. data/lib/generators/not_pressed/plugin/plugin_generator.rb +37 -0
  127. data/lib/generators/not_pressed/plugin/templates/plugin.rb.tt +23 -0
  128. data/lib/not_pressed/admin/authentication.rb +48 -0
  129. data/lib/not_pressed/admin/menu_registry.rb +100 -0
  130. data/lib/not_pressed/configuration.rb +77 -0
  131. data/lib/not_pressed/content_type.rb +23 -0
  132. data/lib/not_pressed/content_type_builder.rb +51 -0
  133. data/lib/not_pressed/content_type_registry.rb +45 -0
  134. data/lib/not_pressed/engine.rb +132 -0
  135. data/lib/not_pressed/hooks.rb +166 -0
  136. data/lib/not_pressed/navigation/builder.rb +148 -0
  137. data/lib/not_pressed/navigation/menu.rb +54 -0
  138. data/lib/not_pressed/navigation/menu_item.rb +33 -0
  139. data/lib/not_pressed/navigation/node.rb +45 -0
  140. data/lib/not_pressed/navigation/partial_parser.rb +96 -0
  141. data/lib/not_pressed/navigation/route_inspector.rb +98 -0
  142. data/lib/not_pressed/navigation.rb +6 -0
  143. data/lib/not_pressed/plugin.rb +354 -0
  144. data/lib/not_pressed/plugin_importer.rb +133 -0
  145. data/lib/not_pressed/plugin_manager.rb +196 -0
  146. data/lib/not_pressed/plugin_packager.rb +129 -0
  147. data/lib/not_pressed/rendering/block_renderer.rb +222 -0
  148. data/lib/not_pressed/rendering/renderer_registry.rb +154 -0
  149. data/lib/not_pressed/rendering.rb +8 -0
  150. data/lib/not_pressed/seo/sitemap_builder.rb +61 -0
  151. data/lib/not_pressed/theme.rb +191 -0
  152. data/lib/not_pressed/theme_importer.rb +133 -0
  153. data/lib/not_pressed/theme_packager.rb +180 -0
  154. data/lib/not_pressed/theme_registry.rb +123 -0
  155. data/lib/not_pressed/version.rb +5 -0
  156. data/lib/not_pressed.rb +65 -0
  157. metadata +258 -0
@@ -0,0 +1,745 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title><%= @page_title ? "#{@page_title} | #{NotPressed.configuration.site_name}" : NotPressed.configuration.site_name %></title>
7
+ <%= stylesheet_link_tag "not_pressed/admin", media: "all" %>
8
+ <%= csrf_meta_tags %>
9
+ <%= csp_meta_tag %>
10
+ <script src="https://unpkg.com/@hotwired/stimulus@3/dist/stimulus.umd.js"></script>
11
+ <script>
12
+ document.addEventListener("DOMContentLoaded", function() {
13
+ var application = Stimulus.Application.start();
14
+
15
+ application.register("np-flash", class extends Stimulus.Controller {
16
+ connect() {
17
+ this.timeout = setTimeout(() => this.dismiss(), 5000);
18
+ }
19
+
20
+ disconnect() {
21
+ if (this.timeout) clearTimeout(this.timeout);
22
+ }
23
+
24
+ dismiss() {
25
+ this.element.style.transition = "opacity 0.3s";
26
+ this.element.style.opacity = "0";
27
+ setTimeout(() => this.element.remove(), 300);
28
+ }
29
+ });
30
+
31
+ application.register("np-sidebar", class extends Stimulus.Controller {
32
+ static get targets() { return ["panel"]; }
33
+
34
+ toggle() {
35
+ if (this.hasPanelTarget) {
36
+ this.panelTarget.classList.toggle("np-sidebar--open");
37
+ }
38
+ }
39
+ });
40
+
41
+ application.register("np-modal", class extends Stimulus.Controller {
42
+ open() {
43
+ this.element.showModal();
44
+ }
45
+
46
+ close() {
47
+ this.element.close();
48
+ }
49
+ });
50
+
51
+ application.register("np-block-editor", class extends Stimulus.Controller {
52
+ static get values() { return { pageId: Number }; }
53
+
54
+ connect() {
55
+ this._handleKeyboard = this._onKeydown.bind(this);
56
+ document.addEventListener("keydown", this._handleKeyboard);
57
+ }
58
+
59
+ disconnect() {
60
+ document.removeEventListener("keydown", this._handleKeyboard);
61
+ }
62
+
63
+ openPicker() {
64
+ var dialog = this.element.querySelector("dialog.np-block-picker");
65
+ if (dialog) dialog.showModal();
66
+ }
67
+
68
+ _onKeydown(e) {
69
+ var mod = e.ctrlKey || e.metaKey;
70
+ // Ctrl+S / Cmd+S — save focused block
71
+ if (mod && e.key === "s") {
72
+ e.preventDefault();
73
+ var activeBlock = document.activeElement.closest(".np-block-body");
74
+ if (activeBlock) {
75
+ var saveBtn = activeBlock.querySelector('input[type="submit"]');
76
+ if (saveBtn) saveBtn.click();
77
+ }
78
+ }
79
+ // Ctrl+Shift+Enter — open block picker
80
+ if (mod && e.shiftKey && e.key === "Enter") {
81
+ e.preventDefault();
82
+ this.openPicker();
83
+ }
84
+ // Delete on focused block header — remove block
85
+ if (e.key === "Delete" && document.activeElement.closest(".np-block-header")) {
86
+ var deleteBtn = document.activeElement.closest(".np-block").querySelector(".np-btn--danger");
87
+ if (deleteBtn) deleteBtn.click();
88
+ }
89
+ }
90
+ });
91
+
92
+ application.register("np-rich-text", class extends Stimulus.Controller {
93
+ static get targets() { return ["editor", "textarea"]; }
94
+
95
+ bold() { document.execCommand("bold"); this.sync(); }
96
+ italic() { document.execCommand("italic"); this.sync(); }
97
+ underline() { document.execCommand("underline"); this.sync(); }
98
+ insertUnorderedList() { document.execCommand("insertUnorderedList"); this.sync(); }
99
+ insertOrderedList() { document.execCommand("insertOrderedList"); this.sync(); }
100
+
101
+ link() {
102
+ var url = prompt("Enter URL:");
103
+ if (url) {
104
+ document.execCommand("createLink", false, url);
105
+ this.sync();
106
+ }
107
+ }
108
+
109
+ sync() {
110
+ if (this.hasEditorTarget && this.hasTextareaTarget) {
111
+ this.textareaTarget.value = this.editorTarget.innerHTML;
112
+ }
113
+ }
114
+ });
115
+
116
+ application.register("np-sortable", class extends Stimulus.Controller {
117
+ static get targets() { return ["list", "item"]; }
118
+ static get values() { return { url: String }; }
119
+
120
+ initialize() {
121
+ this._draggedEl = null;
122
+ }
123
+
124
+ connect() {
125
+ this._onDragStart = this._handleDragStart.bind(this);
126
+ this._onDragOver = this._handleDragOver.bind(this);
127
+ this._onDragEnd = this._handleDragEnd.bind(this);
128
+ this._onDrop = this._handleDrop.bind(this);
129
+
130
+ this.element.addEventListener("dragstart", this._onDragStart);
131
+ this.element.addEventListener("dragover", this._onDragOver);
132
+ this.element.addEventListener("dragend", this._onDragEnd);
133
+ this.element.addEventListener("drop", this._onDrop);
134
+ }
135
+
136
+ disconnect() {
137
+ this.element.removeEventListener("dragstart", this._onDragStart);
138
+ this.element.removeEventListener("dragover", this._onDragOver);
139
+ this.element.removeEventListener("dragend", this._onDragEnd);
140
+ this.element.removeEventListener("drop", this._onDrop);
141
+ }
142
+
143
+ grabHandle() {
144
+ // No-op — just ensures drag handle is interactive
145
+ }
146
+
147
+ _handleDragStart(e) {
148
+ var block = e.target.closest("[data-block-id]");
149
+ if (!block) return;
150
+ this._draggedEl = block;
151
+ block.classList.add("np-block--dragging");
152
+ e.dataTransfer.effectAllowed = "move";
153
+ e.dataTransfer.setData("text/plain", block.dataset.blockId);
154
+ }
155
+
156
+ _handleDragOver(e) {
157
+ e.preventDefault();
158
+ e.dataTransfer.dropEffect = "move";
159
+ if (!this._draggedEl || !this.hasListTarget) return;
160
+
161
+ var afterEl = this._getDragAfterElement(this.listTarget, e.clientY);
162
+ // Remove existing indicator
163
+ var old = this.listTarget.querySelector(".np-drop-indicator");
164
+ if (old) old.remove();
165
+
166
+ var indicator = document.createElement("div");
167
+ indicator.className = "np-drop-indicator";
168
+
169
+ if (afterEl == null) {
170
+ this.listTarget.appendChild(indicator);
171
+ } else {
172
+ this.listTarget.insertBefore(indicator, afterEl);
173
+ }
174
+ }
175
+
176
+ _handleDrop(e) {
177
+ e.preventDefault();
178
+ if (!this._draggedEl || !this.hasListTarget) return;
179
+
180
+ var indicator = this.listTarget.querySelector(".np-drop-indicator");
181
+ if (indicator) {
182
+ this.listTarget.insertBefore(this._draggedEl, indicator);
183
+ indicator.remove();
184
+ }
185
+
186
+ this._persistOrder();
187
+ }
188
+
189
+ _handleDragEnd() {
190
+ if (this._draggedEl) {
191
+ this._draggedEl.classList.remove("np-block--dragging");
192
+ this._draggedEl = null;
193
+ }
194
+ var indicator = this.element.querySelector(".np-drop-indicator");
195
+ if (indicator) indicator.remove();
196
+ }
197
+
198
+ _getDragAfterElement(container, y) {
199
+ var elements = Array.from(container.querySelectorAll("[data-block-id]:not(.np-block--dragging)"));
200
+ var result = null;
201
+ var closest = Number.POSITIVE_INFINITY;
202
+
203
+ elements.forEach(function(el) {
204
+ var box = el.getBoundingClientRect();
205
+ var offset = y - box.top - box.height / 2;
206
+ if (offset < 0 && offset > -closest) {
207
+ closest = -offset;
208
+ result = el;
209
+ }
210
+ });
211
+
212
+ return result;
213
+ }
214
+
215
+ _persistOrder() {
216
+ if (!this.hasListTarget || !this.urlValue) return;
217
+ var blockIds = Array.from(this.listTarget.querySelectorAll("[data-block-id]")).map(function(el) {
218
+ return el.dataset.blockId;
219
+ });
220
+
221
+ var token = document.querySelector('meta[name="csrf-token"]');
222
+ fetch(this.urlValue, {
223
+ method: "PATCH",
224
+ headers: {
225
+ "Content-Type": "application/json",
226
+ "Accept": "text/vnd.turbo-stream.html, text/html",
227
+ "X-CSRF-Token": token ? token.content : ""
228
+ },
229
+ body: JSON.stringify({ block_ids: blockIds })
230
+ });
231
+ }
232
+ });
233
+
234
+ application.register("np-preview", class extends Stimulus.Controller {
235
+ static get targets() { return ["container", "editorPane", "previewPane", "frame", "editorBtn", "splitBtn", "previewBtn"]; }
236
+ static get values() { return { previewUrl: String }; }
237
+
238
+ editorMode() {
239
+ this.editorPaneTarget.style.display = "";
240
+ this.editorPaneTarget.style.flex = "";
241
+ this.previewPaneTarget.style.display = "none";
242
+ this.containerTarget.classList.remove("np-editor-split--active");
243
+ this._setActiveBtn(this.editorBtnTarget);
244
+ }
245
+
246
+ splitMode() {
247
+ this.editorPaneTarget.style.display = "";
248
+ this.editorPaneTarget.style.flex = "0 0 60%";
249
+ this.previewPaneTarget.style.display = "";
250
+ this.previewPaneTarget.style.flex = "0 0 40%";
251
+ this.containerTarget.classList.add("np-editor-split--active");
252
+ this._setActiveBtn(this.splitBtnTarget);
253
+ this.refresh();
254
+ }
255
+
256
+ previewMode() {
257
+ this.editorPaneTarget.style.display = "none";
258
+ this.previewPaneTarget.style.display = "";
259
+ this.previewPaneTarget.style.flex = "1";
260
+ this.containerTarget.classList.add("np-editor-split--active");
261
+ this._setActiveBtn(this.previewBtnTarget);
262
+ this.refresh();
263
+ }
264
+
265
+ refresh() {
266
+ if (this.hasFrameTarget && this.previewUrlValue) {
267
+ this.frameTarget.src = this.previewUrlValue;
268
+ }
269
+ }
270
+
271
+ _setActiveBtn(active) {
272
+ [this.editorBtnTarget, this.splitBtnTarget, this.previewBtnTarget].forEach(function(btn) {
273
+ btn.classList.remove("np-preview-btn--active");
274
+ });
275
+ active.classList.add("np-preview-btn--active");
276
+ }
277
+ });
278
+
279
+ application.register("np-tree", class extends Stimulus.Controller {
280
+ static get targets() { return ["toggle", "children"]; }
281
+
282
+ connect() {
283
+ this._restoreState();
284
+ }
285
+
286
+ toggle(event) {
287
+ var btn = event.currentTarget;
288
+ var pageId = btn.dataset.pageId;
289
+ var container = this.childrenTargets.find(function(el) {
290
+ return el.dataset.parentId === pageId;
291
+ });
292
+ if (!container) return;
293
+
294
+ var collapsed = container.classList.toggle("np-tree-children--collapsed");
295
+ btn.innerHTML = collapsed ? "&#9654;" : "&#9660;";
296
+ this._saveState(pageId, !collapsed);
297
+ }
298
+
299
+ expandAll() {
300
+ var self = this;
301
+ this.childrenTargets.forEach(function(el) {
302
+ el.classList.remove("np-tree-children--collapsed");
303
+ });
304
+ this.toggleTargets.forEach(function(btn) {
305
+ btn.innerHTML = "&#9660;";
306
+ });
307
+ self._clearState();
308
+ }
309
+
310
+ collapseAll() {
311
+ var self = this;
312
+ this.childrenTargets.forEach(function(el) {
313
+ el.classList.add("np-tree-children--collapsed");
314
+ });
315
+ this.toggleTargets.forEach(function(btn) {
316
+ btn.innerHTML = "&#9654;";
317
+ });
318
+ self._clearState();
319
+ this.childrenTargets.forEach(function(el) {
320
+ self._saveState(el.dataset.parentId, false);
321
+ });
322
+ }
323
+
324
+ _restoreState() {
325
+ var state = this._loadState();
326
+ var self = this;
327
+ this.childrenTargets.forEach(function(el) {
328
+ var pageId = el.dataset.parentId;
329
+ if (state[pageId] === false) {
330
+ el.classList.add("np-tree-children--collapsed");
331
+ var btn = self.toggleTargets.find(function(t) { return t.dataset.pageId === pageId; });
332
+ if (btn) btn.innerHTML = "&#9654;";
333
+ }
334
+ });
335
+ }
336
+
337
+ _loadState() {
338
+ try {
339
+ return JSON.parse(localStorage.getItem("np-tree-state") || "{}");
340
+ } catch(e) { return {}; }
341
+ }
342
+
343
+ _saveState(pageId, expanded) {
344
+ var state = this._loadState();
345
+ state[pageId] = expanded;
346
+ localStorage.setItem("np-tree-state", JSON.stringify(state));
347
+ }
348
+
349
+ _clearState() {
350
+ localStorage.removeItem("np-tree-state");
351
+ }
352
+ });
353
+
354
+ application.register("np-page-sortable", class extends Stimulus.Controller {
355
+ static get values() { return { url: String }; }
356
+
357
+ initialize() {
358
+ this._draggedEl = null;
359
+ this._dropZone = null;
360
+ }
361
+
362
+ grabHandle() {
363
+ // No-op — ensures drag handle is interactive
364
+ }
365
+
366
+ dragstart(event) {
367
+ var node = event.currentTarget.closest(".np-tree-node");
368
+ if (!node) return;
369
+ this._draggedEl = node;
370
+ node.classList.add("np-tree-node--dragging");
371
+ event.dataTransfer.effectAllowed = "move";
372
+ event.dataTransfer.setData("text/plain", node.dataset.pageId);
373
+ }
374
+
375
+ dragover(event) {
376
+ event.preventDefault();
377
+ event.dataTransfer.dropEffect = "move";
378
+ if (!this._draggedEl) return;
379
+
380
+ var node = event.target.closest(".np-tree-node");
381
+ if (!node || node === this._draggedEl) {
382
+ this._clearIndicators();
383
+ return;
384
+ }
385
+
386
+ var rect = node.getBoundingClientRect();
387
+ var y = event.clientY - rect.top;
388
+ var height = rect.height;
389
+ var zone;
390
+
391
+ if (y < height * 0.25) {
392
+ zone = "before";
393
+ } else if (y > height * 0.75) {
394
+ zone = "after";
395
+ } else {
396
+ zone = "inside";
397
+ }
398
+
399
+ this._clearIndicators();
400
+ this._dropZone = { node: node, zone: zone };
401
+
402
+ if (zone === "inside") {
403
+ node.classList.add("np-tree-node--drop-inside");
404
+ } else {
405
+ var indicator = document.createElement("div");
406
+ indicator.className = "np-tree-drop-indicator";
407
+ if (zone === "before") {
408
+ node.parentNode.insertBefore(indicator, node);
409
+ } else {
410
+ node.parentNode.insertBefore(indicator, node.nextSibling);
411
+ }
412
+ }
413
+ }
414
+
415
+ drop(event) {
416
+ event.preventDefault();
417
+ if (!this._draggedEl || !this._dropZone) return;
418
+
419
+ var draggedId = this._draggedEl.dataset.pageId;
420
+ var targetNode = this._dropZone.node;
421
+ var zone = this._dropZone.zone;
422
+ var targetId = targetNode.dataset.pageId;
423
+
424
+ if (draggedId === targetId) return;
425
+
426
+ // Move DOM element
427
+ var draggedGroup = this._getNodeWithChildren(this._draggedEl);
428
+
429
+ if (zone === "before") {
430
+ targetNode.parentNode.insertBefore(draggedGroup, targetNode);
431
+ } else if (zone === "after") {
432
+ var targetChildren = targetNode.nextElementSibling;
433
+ if (targetChildren && targetChildren.classList.contains("np-tree-children")) {
434
+ targetChildren.parentNode.insertBefore(draggedGroup, targetChildren.nextSibling);
435
+ } else {
436
+ targetNode.parentNode.insertBefore(draggedGroup, targetNode.nextSibling);
437
+ }
438
+ } else if (zone === "inside") {
439
+ var childrenContainer = targetNode.nextElementSibling;
440
+ if (childrenContainer && childrenContainer.classList.contains("np-tree-children")) {
441
+ childrenContainer.appendChild(draggedGroup);
442
+ } else {
443
+ var newContainer = document.createElement("div");
444
+ newContainer.className = "np-tree-children";
445
+ newContainer.dataset.npTreeTarget = "children";
446
+ newContainer.dataset.parentId = targetId;
447
+ newContainer.appendChild(draggedGroup);
448
+ targetNode.parentNode.insertBefore(newContainer, targetNode.nextSibling);
449
+ }
450
+ }
451
+
452
+ this._clearIndicators();
453
+ this._persistOrder();
454
+ }
455
+
456
+ dragend() {
457
+ if (this._draggedEl) {
458
+ this._draggedEl.classList.remove("np-tree-node--dragging");
459
+ this._draggedEl = null;
460
+ }
461
+ this._dropZone = null;
462
+ this._clearIndicators();
463
+ }
464
+
465
+ _getNodeWithChildren(node) {
466
+ // Create a fragment with the node (children container is separate in DOM)
467
+ var fragment = document.createDocumentFragment();
468
+ var nextSibling = node.nextElementSibling;
469
+ fragment.appendChild(node);
470
+ if (nextSibling && nextSibling.classList.contains("np-tree-children") && nextSibling.dataset.parentId === node.dataset.pageId) {
471
+ fragment.appendChild(nextSibling);
472
+ }
473
+ return fragment;
474
+ }
475
+
476
+ _clearIndicators() {
477
+ var indicators = this.element.querySelectorAll(".np-tree-drop-indicator");
478
+ indicators.forEach(function(el) { el.remove(); });
479
+ var insideNodes = this.element.querySelectorAll(".np-tree-node--drop-inside");
480
+ insideNodes.forEach(function(el) { el.classList.remove("np-tree-node--drop-inside"); });
481
+ }
482
+
483
+ _persistOrder() {
484
+ if (!this.urlValue) return;
485
+
486
+ var pages = [];
487
+ this._walkTree(this.element, null, pages);
488
+
489
+ var token = document.querySelector('meta[name="csrf-token"]');
490
+ fetch(this.urlValue, {
491
+ method: "PATCH",
492
+ headers: {
493
+ "Content-Type": "application/json",
494
+ "Accept": "application/json",
495
+ "X-CSRF-Token": token ? token.content : ""
496
+ },
497
+ body: JSON.stringify({ pages: pages })
498
+ });
499
+ }
500
+
501
+ _walkTree(container, parentId, pages) {
502
+ var self = this;
503
+ var position = 0;
504
+ var children = Array.from(container.children);
505
+ children.forEach(function(el) {
506
+ if (el.classList.contains("np-tree-node")) {
507
+ pages.push({ id: parseInt(el.dataset.pageId), position: position, parent_id: parentId });
508
+ position++;
509
+ // Check for children container immediately after
510
+ var next = el.nextElementSibling;
511
+ if (next && next.classList.contains("np-tree-children")) {
512
+ self._walkTree(next, parseInt(el.dataset.pageId), pages);
513
+ }
514
+ }
515
+ });
516
+ }
517
+ });
518
+
519
+ application.register("np-block-content", class extends Stimulus.Controller {
520
+ static get targets() { return ["indicator"]; }
521
+ static get values() { return { url: String }; }
522
+
523
+ initialize() {
524
+ this._debounceTimer = null;
525
+ }
526
+
527
+ save() {
528
+ if (this._debounceTimer) clearTimeout(this._debounceTimer);
529
+ this._debounceTimer = setTimeout(() => this._performSave(), 300);
530
+ }
531
+
532
+ _performSave() {
533
+ var form = this.element.querySelector("form");
534
+ if (!form) return;
535
+
536
+ var formData = new FormData(form);
537
+ var token = document.querySelector('meta[name="csrf-token"]');
538
+
539
+ fetch(this.urlValue, {
540
+ method: "PATCH",
541
+ headers: {
542
+ "Accept": "text/vnd.turbo-stream.html, text/html",
543
+ "X-CSRF-Token": token ? token.content : ""
544
+ },
545
+ body: formData
546
+ }).then(function(response) {
547
+ if (response.ok) {
548
+ this._showIndicator("Saved");
549
+ }
550
+ }.bind(this)).catch(function() {
551
+ this._showIndicator("Error");
552
+ }.bind(this));
553
+ }
554
+
555
+ _showIndicator(text) {
556
+ if (!this.hasIndicatorTarget) return;
557
+ this.indicatorTarget.textContent = text;
558
+ this.indicatorTarget.style.opacity = "1";
559
+ setTimeout(function() {
560
+ this.indicatorTarget.style.opacity = "0";
561
+ }.bind(this), 1500);
562
+ }
563
+ });
564
+
565
+ application.register("np-media-picker", class extends Stimulus.Controller {
566
+ static get targets() { return ["urlField", "altField", "mediaIdField", "dialog", "grid"]; }
567
+ static get values() { return { pickerUrl: String }; }
568
+
569
+ openModal() {
570
+ if (!this.hasDialogTarget) return;
571
+ this.dialogTarget.showModal();
572
+ this._loadPicker();
573
+ }
574
+
575
+ closeModal() {
576
+ if (this.hasDialogTarget) this.dialogTarget.close();
577
+ }
578
+
579
+ select(event) {
580
+ var item = event.currentTarget;
581
+ var url = item.dataset.mediaUrl;
582
+ var alt = item.dataset.mediaAlt || "";
583
+ var mediaId = item.dataset.mediaId;
584
+
585
+ if (this.hasUrlFieldTarget) {
586
+ this.urlFieldTarget.value = url;
587
+ this.urlFieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
588
+ }
589
+ if (this.hasAltFieldTarget && alt) {
590
+ this.altFieldTarget.value = alt;
591
+ this.altFieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
592
+ }
593
+ if (this.hasMediaIdFieldTarget) {
594
+ this.mediaIdFieldTarget.value = mediaId;
595
+ }
596
+
597
+ this.closeModal();
598
+ }
599
+
600
+ _loadPicker() {
601
+ if (!this.pickerUrlValue || !this.hasGridTarget) return;
602
+ var grid = this.gridTarget;
603
+ fetch(this.pickerUrlValue, {
604
+ headers: { "Accept": "text/html" }
605
+ }).then(function(response) {
606
+ return response.text();
607
+ }).then(function(html) {
608
+ grid.innerHTML = html;
609
+ });
610
+ }
611
+ });
612
+
613
+ application.register("np-media-upload", class extends Stimulus.Controller {
614
+ static get targets() { return ["input"]; }
615
+
616
+ choose() {
617
+ if (this.hasInputTarget) this.inputTarget.click();
618
+ }
619
+
620
+ submit() {
621
+ var form = this.inputTarget.closest("form");
622
+ if (form) form.submit();
623
+ }
624
+ });
625
+
626
+ application.register("np-form-fields", class extends Stimulus.Controller {
627
+ static get targets() { return ["list", "item", "template", "position", "destroy", "optionsGroup"]; }
628
+
629
+ initialize() {
630
+ this._draggedEl = null;
631
+ this._nextIndex = 0;
632
+ }
633
+
634
+ connect() {
635
+ this._nextIndex = this.itemTargets.length;
636
+ }
637
+
638
+ addField() {
639
+ var template = this.templateTarget.innerHTML;
640
+ var html = template.replace(/NEW_INDEX/g, String(Date.now()));
641
+ this.listTarget.insertAdjacentHTML("beforeend", html);
642
+ this._updatePositions();
643
+ }
644
+
645
+ removeField(event) {
646
+ var row = event.target.closest(".np-field-row");
647
+ if (!row) return;
648
+ var destroyInput = row.querySelector('[name*="_destroy"]');
649
+ if (destroyInput) {
650
+ destroyInput.value = "1";
651
+ row.style.display = "none";
652
+ } else {
653
+ row.remove();
654
+ }
655
+ this._updatePositions();
656
+ }
657
+
658
+ toggleOptions(event) {
659
+ var row = event.target.closest(".np-field-row");
660
+ if (!row) return;
661
+ var optionsGroup = row.querySelector('[data-np-form-fields-target="optionsGroup"]');
662
+ if (!optionsGroup) return;
663
+ var optionTypes = ["select", "radio", "checkbox"];
664
+ optionsGroup.style.display = optionTypes.indexOf(event.target.value) !== -1 ? "" : "none";
665
+ }
666
+
667
+ dragstart(event) {
668
+ var row = event.target.closest(".np-field-row");
669
+ if (!row) return;
670
+ this._draggedEl = row;
671
+ row.classList.add("np-block--dragging");
672
+ event.dataTransfer.effectAllowed = "move";
673
+ event.dataTransfer.setData("text/plain", "");
674
+ }
675
+
676
+ dragover(event) {
677
+ event.preventDefault();
678
+ event.dataTransfer.dropEffect = "move";
679
+ if (!this._draggedEl) return;
680
+
681
+ var row = event.target.closest(".np-field-row");
682
+ if (!row || row === this._draggedEl) return;
683
+
684
+ var rect = row.getBoundingClientRect();
685
+ var midY = rect.top + rect.height / 2;
686
+
687
+ if (event.clientY < midY) {
688
+ this.listTarget.insertBefore(this._draggedEl, row);
689
+ } else {
690
+ this.listTarget.insertBefore(this._draggedEl, row.nextSibling);
691
+ }
692
+ }
693
+
694
+ drop(event) {
695
+ event.preventDefault();
696
+ this._updatePositions();
697
+ }
698
+
699
+ dragend() {
700
+ if (this._draggedEl) {
701
+ this._draggedEl.classList.remove("np-block--dragging");
702
+ this._draggedEl = null;
703
+ }
704
+ this._updatePositions();
705
+ }
706
+
707
+ _updatePositions() {
708
+ var visibleRows = Array.from(this.listTarget.querySelectorAll(".np-field-row")).filter(function(row) {
709
+ return row.style.display !== "none";
710
+ });
711
+ visibleRows.forEach(function(row, i) {
712
+ var posInput = row.querySelector('[name*="[position]"]');
713
+ if (posInput) posInput.value = i;
714
+ });
715
+ }
716
+ });
717
+ });
718
+ </script>
719
+ </head>
720
+ <body data-controller="np-sidebar">
721
+ <%= render "not_pressed/admin/shared/sidebar" %>
722
+
723
+ <div id="admin-wrapper">
724
+ <header id="admin-header">
725
+ <div class="np-header-left">
726
+ <button class="np-hamburger" data-action="np-sidebar#toggle" aria-label="Toggle sidebar">
727
+ &#9776;
728
+ </button>
729
+ <h2><%= @page_title %></h2>
730
+ </div>
731
+ <div class="user-info">
732
+ <% if current_admin_user %>
733
+ <span><%= current_admin_user %></span>
734
+ <% end %>
735
+ </div>
736
+ </header>
737
+
738
+ <main id="admin-content">
739
+ <%= render "not_pressed/admin/shared/breadcrumbs" %>
740
+ <%= render "not_pressed/admin/shared/flash" %>
741
+ <%= yield %>
742
+ </main>
743
+ </div>
744
+ </body>
745
+ </html>