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,75 @@
1
+ <%= form_with model: @form, url: @form.new_record? ? admin_forms_path : admin_form_path(@form), class: "np-form" do |f| %>
2
+ <% if @form.errors.any? %>
3
+ <div class="np-form-errors">
4
+ <h3><%= pluralize(@form.errors.count, "error") %> prevented this form from being saved:</h3>
5
+ <ul>
6
+ <% @form.errors.full_messages.each do |message| %>
7
+ <li><%= message %></li>
8
+ <% end %>
9
+ </ul>
10
+ </div>
11
+ <% end %>
12
+
13
+ <fieldset class="np-fieldset">
14
+ <legend class="np-legend">General</legend>
15
+
16
+ <div class="np-form-group">
17
+ <%= f.label :name, class: "np-label" %>
18
+ <%= f.text_field :name, class: "np-input" %>
19
+ </div>
20
+
21
+ <div class="np-form-group">
22
+ <%= f.label :description, class: "np-label" %>
23
+ <%= f.text_area :description, class: "np-input", rows: 3 %>
24
+ </div>
25
+
26
+ <div class="np-form-group">
27
+ <%= f.label :status, class: "np-label" %>
28
+ <%= f.select :status, NotPressed::Form.statuses.keys.map { |s| [s.humanize, s] }, {}, class: "np-input" %>
29
+ </div>
30
+ </fieldset>
31
+
32
+ <fieldset class="np-fieldset">
33
+ <legend class="np-legend">Settings</legend>
34
+
35
+ <div class="np-form-group">
36
+ <%= f.label :email_recipient, class: "np-label" %>
37
+ <%= f.email_field :email_recipient, class: "np-input", placeholder: "notifications@example.com" %>
38
+ </div>
39
+
40
+ <div class="np-form-group">
41
+ <%= f.label :redirect_url, "Redirect URL", class: "np-label" %>
42
+ <%= f.text_field :redirect_url, class: "np-input", placeholder: "/thank-you" %>
43
+ </div>
44
+
45
+ <div class="np-form-group">
46
+ <%= f.label :success_message, class: "np-label" %>
47
+ <%= f.text_area :success_message, class: "np-input", rows: 2, placeholder: "Thank you for your submission!" %>
48
+ </div>
49
+ </fieldset>
50
+
51
+ <fieldset class="np-fieldset" data-controller="np-form-fields">
52
+ <legend class="np-legend">Fields</legend>
53
+
54
+ <div class="np-field-list" data-np-form-fields-target="list">
55
+ <% @form.form_fields.ordered.each.with_index do |field, index| %>
56
+ <%= f.fields_for :form_fields, field, child_index: index do |ff| %>
57
+ <%= render "not_pressed/admin/forms/field_row", ff: ff, index: index %>
58
+ <% end %>
59
+ <% end %>
60
+ </div>
61
+
62
+ <template data-np-form-fields-target="template">
63
+ <%= f.fields_for :form_fields, NotPressed::FormField.new, child_index: "NEW_INDEX" do |ff| %>
64
+ <%= render "not_pressed/admin/forms/field_row", ff: ff, index: "NEW_INDEX" %>
65
+ <% end %>
66
+ </template>
67
+
68
+ <button type="button" class="np-btn" data-action="np-form-fields#addField" style="margin-top: 0.5rem;">+ Add Field</button>
69
+ </fieldset>
70
+
71
+ <div class="np-form-actions">
72
+ <%= f.submit @form.persisted? ? "Update Form" : "Create Form", class: "np-btn np-btn--primary" %>
73
+ <%= link_to "Cancel", admin_forms_path, class: "np-btn" %>
74
+ </div>
75
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render "form" %>
@@ -0,0 +1,32 @@
1
+ <div class="np-actions" style="margin-bottom: 1.5rem;">
2
+ <%= link_to "New Form", new_admin_form_path, class: "np-btn np-btn--primary" %>
3
+ </div>
4
+
5
+ <% if @forms.any? %>
6
+ <table class="np-table">
7
+ <thead>
8
+ <tr>
9
+ <th>Name</th>
10
+ <th>Status</th>
11
+ <th>Submissions</th>
12
+ <th>Actions</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% @forms.each do |form| %>
17
+ <tr>
18
+ <td><%= link_to form.name, edit_admin_form_path(form) %></td>
19
+ <td><%= status_badge(form.status) %></td>
20
+ <td><%= form.form_submissions.count %></td>
21
+ <td>
22
+ <%= link_to "Edit", edit_admin_form_path(form), class: "np-btn np-btn--small" %>
23
+ <%= link_to "Submissions", submissions_admin_form_path(form), class: "np-btn np-btn--small" %>
24
+ <%= link_to "Delete", admin_form_path(form), data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete this form?" }, class: "np-btn np-btn--small np-btn--danger" %>
25
+ </td>
26
+ </tr>
27
+ <% end %>
28
+ </tbody>
29
+ </table>
30
+ <% else %>
31
+ <p>No forms yet. <%= link_to "Create your first form", new_admin_form_path %>.</p>
32
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render "form" %>
@@ -0,0 +1,34 @@
1
+ <div class="np-actions" style="margin-bottom: 1.5rem;">
2
+ <%= link_to "Back to Forms", admin_forms_path, class: "np-btn" %>
3
+ <%= link_to "Edit Form", edit_admin_form_path(@form), class: "np-btn" %>
4
+ <% if @submissions.any? %>
5
+ <%= link_to "Export CSV", export_csv_admin_form_path(@form), class: "np-btn np-btn--primary" %>
6
+ <% end %>
7
+ </div>
8
+
9
+ <h2>Submissions for "<%= @form.name %>"</h2>
10
+
11
+ <% if @submissions.any? %>
12
+ <table class="np-table">
13
+ <thead>
14
+ <tr>
15
+ <% @field_labels.each do |label| %>
16
+ <th><%= label %></th>
17
+ <% end %>
18
+ <th>Submitted At</th>
19
+ </tr>
20
+ </thead>
21
+ <tbody>
22
+ <% @submissions.each do |submission| %>
23
+ <tr>
24
+ <% @field_labels.each do |label| %>
25
+ <td><%= submission.data[label] %></td>
26
+ <% end %>
27
+ <td><%= submission.submitted_at&.strftime("%Y-%m-%d %H:%M:%S") %></td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
32
+ <% else %>
33
+ <p>No submissions yet.</p>
34
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <div class="np-media-card">
2
+ <div class="np-media-card-thumb">
3
+ <% if media.image? && media.file.attached? %>
4
+ <%= image_tag main_app.rails_blob_path(media.file, disposition: "inline", only_path: true), alt: media.alt_text || media.title %>
5
+ <% else %>
6
+ <div class="np-media-card-icon">DOC</div>
7
+ <% end %>
8
+ </div>
9
+ <div class="np-media-card-info">
10
+ <div class="np-media-card-title"><%= media.title || "Untitled" %></div>
11
+ <div class="np-media-card-meta">
12
+ <span class="np-badge"><%= media.content_type %></span>
13
+ <span><%= number_to_human_size(media.file_size || 0) %></span>
14
+ </div>
15
+ <div class="np-media-card-date"><%= time_ago_short(media.created_at) %></div>
16
+ </div>
17
+ <div class="np-media-card-actions">
18
+ <%= link_to "Edit", edit_admin_media_attachment_path(media), class: "np-btn np-btn--small" %>
19
+ <%= button_to "Delete", admin_media_attachment_path(media), method: :delete, class: "np-btn np-btn--small np-btn--danger", data: { confirm: "Delete this media?" } %>
20
+ </div>
21
+ </div>
@@ -0,0 +1,19 @@
1
+ <div class="np-media-picker-grid" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(120px, 1fr)); gap:0.75rem; padding:1rem;">
2
+ <% @media.each do |media| %>
3
+ <div class="np-media-picker-item"
4
+ style="cursor:pointer; border:2px solid transparent; border-radius:4px; overflow:hidden; transition:border-color 0.2s;"
5
+ data-media-id="<%= media.id %>"
6
+ data-media-url="<%= main_app.rails_blob_path(media.file, disposition: 'inline', only_path: true) %>"
7
+ data-media-alt="<%= media.alt_text %>"
8
+ data-media-title="<%= media.title %>"
9
+ data-action="click->np-media-picker#select">
10
+ <%= image_tag main_app.rails_blob_path(media.file, disposition: "inline", only_path: true), alt: media.alt_text || media.title, style: "width:100%; height:100px; object-fit:cover; display:block;" %>
11
+ <div style="padding:0.25rem; font-size:0.75rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
12
+ <%= media.title %>
13
+ </div>
14
+ </div>
15
+ <% end %>
16
+ <% if @media.empty? %>
17
+ <p style="grid-column:1/-1; text-align:center; color:#64748b; padding:2rem 0;">No images found in media library.</p>
18
+ <% end %>
19
+ </div>
@@ -0,0 +1,57 @@
1
+ <% admin_breadcrumb "Media Library", admin_media_attachments_path %>
2
+ <% admin_breadcrumb @media_attachment.title || "Edit" %>
3
+
4
+ <div style="display:flex; gap:2rem; flex-wrap:wrap;">
5
+ <div style="flex:0 0 300px;">
6
+ <div class="np-media-card-thumb" style="height:auto; max-height:300px; border:1px solid var(--np-border); border-radius:0.5rem; overflow:hidden; display:flex; align-items:center; justify-content:center; background-color:#f1f5f9;">
7
+ <% if @media_attachment.image? && @media_attachment.file.attached? %>
8
+ <%= image_tag main_app.rails_blob_path(@media_attachment.file, disposition: "inline", only_path: true), style: "max-width:100%; max-height:300px; object-fit:contain;" %>
9
+ <% else %>
10
+ <div style="padding:2rem; text-align:center; color:#94a3b8;">
11
+ <div style="font-size:2rem; font-weight:700;">DOC</div>
12
+ <div style="margin-top:0.5rem;"><%= @media_attachment.content_type %></div>
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+
17
+ <div style="margin-top:1rem; font-size:0.875rem; color:#64748b;">
18
+ <p><strong>File size:</strong> <%= number_to_human_size(@media_attachment.file_size || 0) %></p>
19
+ <p><strong>Content type:</strong> <%= @media_attachment.content_type %></p>
20
+ <p><strong>Uploaded:</strong> <%= @media_attachment.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
21
+ <% if @media_attachment.file.attached? %>
22
+ <p><strong>URL:</strong> <%= link_to "View file", main_app.rails_blob_path(@media_attachment.file, disposition: "inline", only_path: true), target: "_blank", class: "np-btn np-btn--small" %></p>
23
+ <% end %>
24
+ </div>
25
+ </div>
26
+
27
+ <div style="flex:1; min-width:250px;">
28
+ <%= form_with model: @media_attachment, url: admin_media_attachment_path(@media_attachment), method: :patch, class: "np-form" do |f| %>
29
+ <% if @media_attachment.errors.any? %>
30
+ <div class="np-form-errors">
31
+ <h3><%= pluralize(@media_attachment.errors.count, "error") %> prevented this media from being saved:</h3>
32
+ <ul>
33
+ <% @media_attachment.errors.full_messages.each do |msg| %>
34
+ <li><%= msg %></li>
35
+ <% end %>
36
+ </ul>
37
+ </div>
38
+ <% end %>
39
+
40
+ <div class="np-form-group">
41
+ <%= f.label :title, class: "np-label" %>
42
+ <%= f.text_field :title, class: "np-input" %>
43
+ </div>
44
+
45
+ <div class="np-form-group">
46
+ <%= f.label :alt_text, "Alt text", class: "np-label" %>
47
+ <%= f.text_area :alt_text, class: "np-input", rows: 3, placeholder: "Describe this media for accessibility" %>
48
+ </div>
49
+
50
+ <div class="np-form-actions">
51
+ <%= f.submit "Save Changes", class: "np-btn np-btn--primary" %>
52
+ <%= link_to "Back to Media", admin_media_attachments_path, class: "np-btn" %>
53
+ <%= button_to "Delete", admin_media_attachment_path(@media_attachment), method: :delete, class: "np-btn np-btn--danger", data: { confirm: "Delete this media?" } %>
54
+ </div>
55
+ <% end %>
56
+ </div>
57
+ </div>
@@ -0,0 +1,48 @@
1
+ <% admin_breadcrumb "Media Library" %>
2
+
3
+ <div class="np-actions" style="margin-bottom: 1rem;">
4
+ <div data-controller="np-media-upload">
5
+ <%= form_tag admin_media_attachments_path, multipart: true, id: "media-upload-form", style: "display:inline" do %>
6
+ <input type="file" name="files[]" multiple id="media-file-input" style="display:none" data-np-media-upload-target="input" data-action="change->np-media-upload#submit">
7
+ <button type="button" class="np-btn np-btn--primary" data-action="np-media-upload#choose">Upload Files</button>
8
+ <% end %>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="np-media-filters" style="display:flex; gap:1rem; align-items:center; margin-bottom:1rem; flex-wrap:wrap;">
13
+ <div class="np-media-tabs">
14
+ <%= link_to "All", admin_media_attachments_path(q: params[:q]), class: "np-btn np-btn--small #{params[:type].blank? ? 'np-btn--primary' : ''}" %>
15
+ <%= link_to "Images", admin_media_attachments_path(type: "images", q: params[:q]), class: "np-btn np-btn--small #{params[:type] == 'images' ? 'np-btn--primary' : ''}" %>
16
+ <%= link_to "Documents", admin_media_attachments_path(type: "documents", q: params[:q]), class: "np-btn np-btn--small #{params[:type] == 'documents' ? 'np-btn--primary' : ''}" %>
17
+ </div>
18
+
19
+ <%= form_tag admin_media_attachments_path, method: :get, style: "display:flex; gap:0.5rem;" do %>
20
+ <% if params[:type].present? %>
21
+ <input type="hidden" name="type" value="<%= params[:type] %>">
22
+ <% end %>
23
+ <input type="text" name="q" value="<%= params[:q] %>" placeholder="Search media..." class="np-input" style="width:14rem;">
24
+ <button type="submit" class="np-btn">Search</button>
25
+ <% end %>
26
+ </div>
27
+
28
+ <% if @media.any? %>
29
+ <div class="np-media-grid">
30
+ <% @media.each do |media| %>
31
+ <%= render "media_card", media: media %>
32
+ <% end %>
33
+ </div>
34
+
35
+ <% if @total_pages > 1 %>
36
+ <div class="np-pagination" style="display:flex; gap:0.5rem; margin-top:1rem; align-items:center;">
37
+ <% if @current_page > 1 %>
38
+ <%= link_to "Previous", admin_media_attachments_path(page: @current_page - 1, type: params[:type], q: params[:q]), class: "np-btn np-btn--small" %>
39
+ <% end %>
40
+ <span style="font-size:0.875rem; color:#64748b;">Page <%= @current_page %> of <%= @total_pages %></span>
41
+ <% if @current_page < @total_pages %>
42
+ <%= link_to "Next", admin_media_attachments_path(page: @current_page + 1, type: params[:type], q: params[:q]), class: "np-btn np-btn--small" %>
43
+ <% end %>
44
+ </div>
45
+ <% end %>
46
+ <% else %>
47
+ <p style="color:#64748b; text-align:center; padding:2rem 0;">No media found.</p>
48
+ <% end %>
@@ -0,0 +1,177 @@
1
+ <%= form_with model: @page, url: @page.new_record? ? admin_pages_path : admin_page_path(@page), class: "np-form" do |f| %>
2
+ <% if @page.errors.any? %>
3
+ <div class="np-form-errors">
4
+ <h3><%= pluralize(@page.errors.count, "error") %> prevented this page from being saved:</h3>
5
+ <ul>
6
+ <% @page.errors.full_messages.each do |message| %>
7
+ <li><%= message %></li>
8
+ <% end %>
9
+ </ul>
10
+ </div>
11
+ <% end %>
12
+
13
+ <fieldset class="np-fieldset">
14
+ <legend class="np-legend">General</legend>
15
+
16
+ <div class="np-form-group">
17
+ <%= f.label :title, class: "np-label" %>
18
+ <%= f.text_field :title, class: "np-input" %>
19
+ </div>
20
+
21
+ <div class="np-form-group">
22
+ <%= f.label :slug, class: "np-label" %>
23
+ <%= f.text_field :slug, class: "np-input", placeholder: "auto-generated from title if blank" %>
24
+ </div>
25
+
26
+ <div class="np-form-group">
27
+ <%= f.label :parent_id, "Parent Page", class: "np-label" %>
28
+ <%= f.select :parent_id, NotPressed::Page.where.not(id: @page.id).pluck(:title, :id), { include_blank: "None (top-level)" }, class: "np-input" %>
29
+ </div>
30
+
31
+ <div class="np-form-group">
32
+ <%= f.label :content_type, class: "np-label" %>
33
+ <%= f.select :content_type, NotPressed::ContentTypeRegistry.names, {}, class: "np-input" %>
34
+ </div>
35
+
36
+ <div class="np-form-group">
37
+ <%= f.label :status, class: "np-label" %>
38
+ <%= f.select :status, NotPressed::Page.statuses.keys.map { |s| [s.humanize, s] }, {}, class: "np-input", id: "page-status-select" %>
39
+ </div>
40
+
41
+ <div class="np-form-group" id="scheduled-at-group" style="<%= @page.scheduled? ? '' : 'display:none;' %>">
42
+ <%= f.label :scheduled_at, "Scheduled Publish Date", class: "np-label" %>
43
+ <%= f.datetime_local_field :scheduled_at, class: "np-input" %>
44
+ </div>
45
+
46
+ <div id="blog-fields" style="<%= @page.content_type == 'blog_post' ? '' : 'display:none;' %>">
47
+ <div class="np-form-group">
48
+ <%= f.label :category_id, "Category", class: "np-label" %>
49
+ <%= f.select :category_id, NotPressed::Category.ordered.pluck(:name, :id), { include_blank: "None" }, class: "np-input" %>
50
+ </div>
51
+
52
+ <div class="np-form-group">
53
+ <span class="np-label">Tags</span>
54
+ <div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
55
+ <% NotPressed::Tag.order(:name).each do |tag| %>
56
+ <label style="display: inline-flex; align-items: center; gap: 0.25rem;">
57
+ <%= check_box_tag "page[tag_ids][]", tag.id, @page.tag_ids.include?(tag.id) %>
58
+ <%= tag.name %>
59
+ </label>
60
+ <% end %>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </fieldset>
65
+
66
+ <details class="np-fieldset-details">
67
+ <summary class="np-legend">SEO</summary>
68
+ <fieldset class="np-fieldset np-fieldset--no-border">
69
+ <div class="np-form-group">
70
+ <%= f.label :meta_title, class: "np-label" %>
71
+ <%= f.text_field :meta_title, class: "np-input", placeholder: "Defaults to page title" %>
72
+ </div>
73
+
74
+ <div class="np-form-group">
75
+ <%= f.label :meta_description, class: "np-label" %>
76
+ <%= f.text_area :meta_description, class: "np-input", rows: 2 %>
77
+ </div>
78
+
79
+ <div class="np-form-group">
80
+ <%= f.label :og_image_url, "OG Image URL", class: "np-label" %>
81
+ <%= f.text_field :og_image_url, class: "np-input", placeholder: "https://example.com/image.jpg" %>
82
+ </div>
83
+
84
+ <div class="np-form-group">
85
+ <%= f.label :canonical_url, "Canonical URL", class: "np-label" %>
86
+ <%= f.text_field :canonical_url, class: "np-input", placeholder: "https://example.com/preferred-url" %>
87
+ </div>
88
+
89
+ <div class="np-form-group">
90
+ <%= f.label :meta_robots, "Meta Robots", class: "np-label" %>
91
+ <%= f.text_field :meta_robots, class: "np-input", placeholder: "e.g. noindex, nofollow" %>
92
+ </div>
93
+
94
+ <div class="np-form-group">
95
+ <%= f.label :og_type, "OG Type", class: "np-label" %>
96
+ <%= f.select :og_type, ["website", "article", "profile"], {}, class: "np-input" %>
97
+ </div>
98
+
99
+ <div class="np-form-group">
100
+ <%= f.label :twitter_card, "Twitter Card", class: "np-label" %>
101
+ <%= f.select :twitter_card, ["summary", "summary_large_image"], {}, class: "np-input" %>
102
+ </div>
103
+ </fieldset>
104
+ </details>
105
+
106
+ <details class="np-fieldset-details">
107
+ <summary class="np-legend">Code Injection</summary>
108
+ <fieldset class="np-fieldset np-fieldset--no-border">
109
+ <p class="np-hint">Add custom scripts, styles, or meta tags for this page.</p>
110
+
111
+ <div class="np-form-group">
112
+ <%= f.label :head_code, "Header Code", class: "np-label" %>
113
+ <p class="np-hint">Injected into &lt;head&gt;</p>
114
+ <%= f.text_area :head_code, class: "np-input", rows: 4, style: "font-family: monospace;" %>
115
+ </div>
116
+
117
+ <div class="np-form-group">
118
+ <%= f.label :body_code, "Footer Code", class: "np-label" %>
119
+ <p class="np-hint">Injected before &lt;/body&gt;</p>
120
+ <%= f.text_area :body_code, class: "np-input", rows: 4, style: "font-family: monospace;" %>
121
+ </div>
122
+ </fieldset>
123
+ </details>
124
+
125
+ <details class="np-fieldset-details">
126
+ <summary class="np-legend">Advanced</summary>
127
+ <fieldset class="np-fieldset np-fieldset--no-border">
128
+ <div class="np-form-group">
129
+ <%= f.label :layout, class: "np-label" %>
130
+ <%= f.select :layout, NotPressed.configuration.effective_layouts, { include_blank: "Default" }, class: "np-input" %>
131
+ </div>
132
+
133
+ <div class="np-form-group">
134
+ <%= f.label :visibility, class: "np-label" %>
135
+ <%= f.select :visibility, NotPressed::Page.visibilities.keys.map { |v| [v.humanize, v] }, {}, class: "np-input", id: "page-visibility-select" %>
136
+ </div>
137
+
138
+ <div class="np-form-group" id="password-group" style="<%= @page.visibility_password_protected? ? '' : 'display:none;' %>">
139
+ <%= f.label :page_password, "Page Password", class: "np-label" %>
140
+ <%= f.text_field :page_password, class: "np-input", placeholder: "Enter password for protected page" %>
141
+ </div>
142
+ </fieldset>
143
+ </details>
144
+
145
+ <script>
146
+ document.addEventListener("DOMContentLoaded", function() {
147
+ var statusField = document.getElementById("page-status-select");
148
+ var scheduledGroup = document.getElementById("scheduled-at-group");
149
+ if (statusField && scheduledGroup) {
150
+ statusField.addEventListener("change", function() {
151
+ scheduledGroup.style.display = statusField.value === "scheduled" ? "" : "none";
152
+ });
153
+ }
154
+
155
+ var contentTypeField = document.querySelector("select[name='page[content_type]']");
156
+ var blogFields = document.getElementById("blog-fields");
157
+ if (contentTypeField && blogFields) {
158
+ contentTypeField.addEventListener("change", function() {
159
+ blogFields.style.display = contentTypeField.value === "blog_post" ? "" : "none";
160
+ });
161
+ }
162
+
163
+ var visibilityField = document.getElementById("page-visibility-select");
164
+ var passwordGroup = document.getElementById("password-group");
165
+ if (visibilityField && passwordGroup) {
166
+ visibilityField.addEventListener("change", function() {
167
+ passwordGroup.style.display = visibilityField.value === "password_protected" ? "" : "none";
168
+ });
169
+ }
170
+ });
171
+ </script>
172
+
173
+ <div class="np-form-actions">
174
+ <%= f.submit @page.persisted? ? "Update Page" : "Create Page", class: "np-btn np-btn--primary" %>
175
+ <%= link_to "Cancel", admin_pages_path, class: "np-btn" %>
176
+ </div>
177
+ <% end %>
@@ -0,0 +1,32 @@
1
+ <div class="np-tree-node" data-tree-id="<%= page.id %>" data-tree-level="<%= level %>" data-page-id="<%= page.id %>" draggable="true" data-action="dragstart->np-page-sortable#dragstart dragover->np-page-sortable#dragover drop->np-page-sortable#drop dragend->np-page-sortable#dragend">
2
+ <div class="np-tree-indent" style="padding-left: <%= level * 1.5 %>rem;">
3
+ <span class="np-drag-handle" data-action="mousedown->np-page-sortable#grabHandle">&#10495;</span>
4
+ <% if page.children.any? %>
5
+ <button type="button" class="np-tree-toggle" data-action="click->np-tree#toggle" data-np-tree-target="toggle" data-page-id="<%= page.id %>" aria-label="Toggle children">&#9660;</button>
6
+ <% else %>
7
+ <span class="np-tree-toggle np-tree-toggle--leaf">&bull;</span>
8
+ <% end %>
9
+ </div>
10
+ <div class="np-tree-title">
11
+ <%= link_to page.title, edit_admin_page_path(page) %>
12
+ <% if page.children.any? %>
13
+ <span class="np-badge"><%= page.children.size %></span>
14
+ <% end %>
15
+ </div>
16
+ <div class="np-tree-meta"><%= status_badge(page.status) %></div>
17
+ <div class="np-tree-meta"><%= page.content_type %></div>
18
+ <div class="np-tree-meta"><%= page.content_blocks.size %></div>
19
+ <div class="np-tree-meta"><%= time_ago_short(page.updated_at) %></div>
20
+ <div class="np-tree-actions">
21
+ <%= link_to "Edit", edit_admin_page_path(page), class: "np-btn np-btn--small" %>
22
+ <%= button_to "Duplicate", duplicate_admin_page_path(page), method: :post, class: "np-btn np-btn--small" %>
23
+ <%= button_to "Delete", admin_page_path(page), method: :delete, class: "np-btn np-btn--danger np-btn--small", data: { turbo_confirm: "Are you sure?" } %>
24
+ </div>
25
+ </div>
26
+ <% if page.children.any? %>
27
+ <div class="np-tree-children" data-np-tree-target="children" data-parent-id="<%= page.id %>">
28
+ <% page.children.ordered.each do |child| %>
29
+ <%= render partial: "not_pressed/admin/pages/page_tree_node", locals: { page: child, level: level + 1 } %>
30
+ <% end %>
31
+ </div>
32
+ <% end %>
@@ -0,0 +1,69 @@
1
+ <div class="np-preview-toggle" data-controller="np-preview" data-np-preview-preview-url-value="<%= preview_admin_page_path(@page) %>">
2
+ <div class="np-preview-toggle-buttons">
3
+ <button type="button" class="np-btn np-btn--small np-preview-btn--active" data-action="click->np-preview#editorMode" data-np-preview-target="editorBtn">Editor</button>
4
+ <button type="button" class="np-btn np-btn--small" data-action="click->np-preview#splitMode" data-np-preview-target="splitBtn">Split</button>
5
+ <button type="button" class="np-btn np-btn--small" data-action="click->np-preview#previewMode" data-np-preview-target="previewBtn">Preview</button>
6
+ </div>
7
+
8
+ <div class="np-editor-split" data-np-preview-target="container">
9
+ <div class="np-editor-pane" data-np-preview-target="editorPane">
10
+ <div class="np-page-settings">
11
+ <details>
12
+ <summary class="np-btn" style="margin-bottom: 1rem;">Page Settings</summary>
13
+ <div class="np-page-settings-body">
14
+ <%= render "form" %>
15
+ </div>
16
+ </details>
17
+ </div>
18
+
19
+ <hr style="margin: 1.5rem 0; border: none; border-top: 1px solid var(--np-border);">
20
+
21
+ <div class="np-block-editor" data-controller="np-block-editor np-sortable" data-np-block-editor-page-id-value="<%= @page.id %>" data-np-sortable-url-value="<%= reorder_admin_page_content_blocks_path(@page) %>">
22
+ <h3 style="margin-bottom: 1rem;">Content Blocks</h3>
23
+
24
+ <div id="blocks-list" data-np-sortable-target="list">
25
+ <% @blocks.each do |block| %>
26
+ <%= render "not_pressed/admin/content_blocks/block", block: block %>
27
+ <% end %>
28
+ </div>
29
+
30
+ <div class="np-block-type-picker" style="margin-top: 1rem;">
31
+ <button type="button" class="np-btn np-btn--primary" data-action="click->np-block-editor#openPicker">+ Add Block</button>
32
+ </div>
33
+
34
+ <%= render "not_pressed/admin/content_blocks/block_picker", page: @page %>
35
+ </div>
36
+ </div>
37
+
38
+ <% if @versions.present? %>
39
+ <hr style="margin: 1.5rem 0; border: none; border-top: 1px solid var(--np-border);">
40
+
41
+ <details>
42
+ <summary class="np-btn" style="margin-bottom: 1rem;">Version History (<%= @versions.size %>)</summary>
43
+ <table class="np-table" style="margin-top: 0.5rem;">
44
+ <thead>
45
+ <tr>
46
+ <th>Version</th>
47
+ <th>Event</th>
48
+ <th>Date</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ <% @versions.each do |version| %>
53
+ <tr>
54
+ <td>#<%= version.version_number %></td>
55
+ <td><span class="np-badge"><%= version.event.humanize %></span></td>
56
+ <td><%= time_ago_short(version.created_at) %></td>
57
+ </tr>
58
+ <% end %>
59
+ </tbody>
60
+ </table>
61
+ </details>
62
+ <% end %>
63
+ </div>
64
+
65
+ <div class="np-preview-pane" data-np-preview-target="previewPane" style="display: none;">
66
+ <iframe data-np-preview-target="frame" class="np-preview-frame" src="<%= preview_admin_page_path(@page) %>"></iframe>
67
+ </div>
68
+ </div>
69
+ </div>
@@ -0,0 +1,21 @@
1
+ <div class="np-actions" style="margin-bottom: 1.5rem;">
2
+ <%= link_to "New Page", new_admin_page_path, class: "np-btn np-btn--primary" %>
3
+ </div>
4
+
5
+ <% if @pages.any? %>
6
+ <div class="np-tree" data-controller="np-tree np-page-sortable" data-np-page-sortable-url-value="<%= reorder_admin_pages_path %>">
7
+ <div class="np-tree-header">
8
+ <div class="np-tree-header-title">Title</div>
9
+ <div class="np-tree-meta">Status</div>
10
+ <div class="np-tree-meta">Type</div>
11
+ <div class="np-tree-meta">Blocks</div>
12
+ <div class="np-tree-meta">Updated</div>
13
+ <div class="np-tree-header-actions">Actions</div>
14
+ </div>
15
+ <% @pages.each do |page| %>
16
+ <%= render partial: "not_pressed/admin/pages/page_tree_node", locals: { page: page, level: 0 } %>
17
+ <% end %>
18
+ </div>
19
+ <% else %>
20
+ <p>No pages yet. <%= link_to "Create your first page", new_admin_page_path %>.</p>
21
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render "form" %>
@@ -0,0 +1,17 @@
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>Preview: <%= @page.title %></title>
7
+ <%= stylesheet_link_tag "not_pressed/content", media: "all" %>
8
+ <%= stylesheet_link_tag "not_pressed/gallery", media: "all" %>
9
+ </head>
10
+ <body class="np-preview-body">
11
+ <article class="np-preview-article">
12
+ <h1><%= @page.title %></h1>
13
+ <%= render_page_content(@page) %>
14
+ </article>
15
+ <%= javascript_include_tag "not_pressed/lightbox" %>
16
+ </body>
17
+ </html>