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,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>NotPressed</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ </head>
9
+ <body>
10
+ <%= yield %>
11
+ </body>
12
+ </html>
@@ -0,0 +1,22 @@
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
+ <%= seo_meta_tags(@page, request) %>
7
+ <%= stylesheet_link_tag "not_pressed/content", media: "all" %>
8
+ <%= stylesheet_link_tag "not_pressed/gallery", media: "all" %>
9
+ <%= theme_stylesheet_tag %>
10
+ <%= theme_color_overrides %>
11
+ <%= csrf_meta_tags %>
12
+ <%= csp_meta_tag %>
13
+ <%= head_injection(@page) %>
14
+ </head>
15
+ <body class="np-page-body <%= active_theme_class %>">
16
+ <main class="np-page-main">
17
+ <%= yield %>
18
+ </main>
19
+ <%= javascript_include_tag "not_pressed/lightbox" %>
20
+ <%= body_injection(@page) %>
21
+ </body>
22
+ </html>
@@ -0,0 +1,58 @@
1
+ <div class="np-actions" style="margin-bottom: 1.5rem;">
2
+ <h2 style="margin: 0;">Add Category</h2>
3
+ </div>
4
+
5
+ <%= form_with model: @category, url: admin_categories_path, class: "np-form", style: "margin-bottom: 2rem;" do |f| %>
6
+ <% if @category.errors.any? %>
7
+ <div class="np-form-errors">
8
+ <ul>
9
+ <% @category.errors.full_messages.each do |message| %>
10
+ <li><%= message %></li>
11
+ <% end %>
12
+ </ul>
13
+ </div>
14
+ <% end %>
15
+
16
+ <div style="display: flex; gap: 0.5rem; align-items: flex-end;">
17
+ <div class="np-form-group" style="margin-bottom: 0; flex: 1;">
18
+ <%= f.label :name, class: "np-label" %>
19
+ <%= f.text_field :name, class: "np-input", placeholder: "Category name" %>
20
+ </div>
21
+ <div class="np-form-group" style="margin-bottom: 0; flex: 2;">
22
+ <%= f.label :description, class: "np-label" %>
23
+ <%= f.text_field :description, class: "np-input", placeholder: "Optional description" %>
24
+ </div>
25
+ <div>
26
+ <%= f.submit "Add", class: "np-btn np-btn--primary" %>
27
+ </div>
28
+ </div>
29
+ <% end %>
30
+
31
+ <% if @categories.any? %>
32
+ <table class="np-table">
33
+ <thead>
34
+ <tr>
35
+ <th>Name</th>
36
+ <th>Slug</th>
37
+ <th>Description</th>
38
+ <th>Posts</th>
39
+ <th>Actions</th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ <% @categories.each do |category| %>
44
+ <tr>
45
+ <td><%= category.name %></td>
46
+ <td><code><%= category.slug %></code></td>
47
+ <td><%= category.description %></td>
48
+ <td><%= category.pages.count %></td>
49
+ <td>
50
+ <%= link_to "Delete", admin_category_path(category), data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete this category?" }, class: "np-btn np-btn--small np-btn--danger" %>
51
+ </td>
52
+ </tr>
53
+ <% end %>
54
+ </tbody>
55
+ </table>
56
+ <% else %>
57
+ <p>No categories yet. Add one above.</p>
58
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <div id="<%= dom_id(block) %>" class="np-block" draggable="true" data-block-id="<%= block.id %>" data-np-sortable-target="item">
2
+ <div class="np-block-header">
3
+ <div class="np-block-header-left">
4
+ <span class="np-drag-handle" data-action="mousedown->np-sortable#grabHandle" title="Drag to reorder">&#9776;</span>
5
+ <span class="np-block-type-label"><%= block.block_type.humanize %></span>
6
+ </div>
7
+ <%= button_to "Remove", admin_page_content_block_path(block.page, block), method: :delete, class: "np-btn np-btn--danger np-btn--small", data: { turbo_confirm: "Remove this block?" } %>
8
+ </div>
9
+ <div class="np-block-body" data-controller="np-block-content" data-np-block-content-url-value="<%= admin_page_content_block_path(block.page, block) %>">
10
+ <%= form_with model: block, url: admin_page_content_block_path(block.page, block), method: :patch, class: "np-block-form" do |f| %>
11
+ <%= render NotPressed::Rendering::RendererRegistry.editor_partial(block.block_type), block: block %>
12
+ <div class="np-form-actions" style="margin-top: 0.5rem;">
13
+ <%= f.submit "Save", class: "np-btn np-btn--primary np-btn--small" %>
14
+ <span class="np-save-indicator" data-np-block-content-target="indicator"></span>
15
+ </div>
16
+ <% end %>
17
+ </div>
18
+ </div>
@@ -0,0 +1,32 @@
1
+ <dialog class="np-modal np-block-picker" data-controller="np-modal">
2
+ <div class="np-modal-header">
3
+ <h3>Add a Block</h3>
4
+ <button type="button" class="np-modal-close" data-action="np-modal#close">&times;</button>
5
+ </div>
6
+ <div class="np-modal-body">
7
+ <div class="np-block-picker-grid">
8
+ <% block_descriptions = {
9
+ "text" => { emoji: "\u{1F4DD}", desc: "Rich text content" },
10
+ "heading" => { emoji: "\u{1F520}", desc: "Section heading" },
11
+ "image" => { emoji: "\u{1F5BC}", desc: "Image with caption" },
12
+ "video" => { emoji: "\u{1F3AC}", desc: "Embedded video" },
13
+ "code" => { emoji: "\u{1F4BB}", desc: "Code snippet" },
14
+ "quote" => { emoji: "\u{1F4AC}", desc: "Blockquote with attribution" },
15
+ "divider" => { emoji: "\u2796", desc: "Horizontal divider" },
16
+ "html" => { emoji: "\u{1F9F1}", desc: "Raw HTML block" },
17
+ "form" => { emoji: "\u{1F4CB}", desc: "Embeddable form" }
18
+ } %>
19
+ <% NotPressed::ContentBlock.registered_types.each do |type| %>
20
+ <% info = block_descriptions[type] || { emoji: "\u{1F4E6}", desc: type.humanize } %>
21
+ <%= button_to admin_page_content_blocks_path(page, block_type: type),
22
+ method: :post,
23
+ class: "np-block-picker-card",
24
+ data: { turbo_stream: true, action: "np-modal#close" } do %>
25
+ <span class="np-block-picker-icon"><%= info[:emoji] %></span>
26
+ <span class="np-block-picker-name"><%= type.humanize %></span>
27
+ <span class="np-block-picker-desc"><%= info[:desc] %></span>
28
+ <% end %>
29
+ <% end %>
30
+ </div>
31
+ </div>
32
+ </dialog>
@@ -0,0 +1,3 @@
1
+ <%= turbo_stream.append "blocks-list" do %>
2
+ <%= render "not_pressed/admin/content_blocks/block", block: @block %>
3
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= turbo_stream.remove dom_id(@block) %>
@@ -0,0 +1,38 @@
1
+ <div class="np-editor-callout">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_style_<%= block.id %>">Style</label>
4
+ <select
5
+ name="content_block[content][style]"
6
+ id="content_style_<%= block.id %>"
7
+ class="np-input"
8
+ data-action="change->np-block-content#save"
9
+ >
10
+ <% %w[info warning success danger].each do |style| %>
11
+ <option value="<%= style %>" <%= "selected" if block.content["style"] == style %>><%= style.capitalize %></option>
12
+ <% end %>
13
+ </select>
14
+ </div>
15
+ <div class="np-editor-field">
16
+ <label class="np-label" for="content_title_<%= block.id %>">Title</label>
17
+ <input
18
+ type="text"
19
+ name="content_block[content][title]"
20
+ id="content_title_<%= block.id %>"
21
+ value="<%= block.content["title"] %>"
22
+ class="np-input"
23
+ placeholder="Callout title"
24
+ data-action="blur->np-block-content#save change->np-block-content#save"
25
+ >
26
+ </div>
27
+ <div class="np-editor-field">
28
+ <label class="np-label" for="content_text_<%= block.id %>">Text</label>
29
+ <textarea
30
+ name="content_block[content][text]"
31
+ id="content_text_<%= block.id %>"
32
+ class="np-input"
33
+ rows="4"
34
+ placeholder="Callout text content"
35
+ data-action="blur->np-block-content#save change->np-block-content#save"
36
+ ><%= block.content["text"] %></textarea>
37
+ </div>
38
+ </div>
@@ -0,0 +1,26 @@
1
+ <div class="np-editor-code">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_language_<%= block.id %>">Language</label>
4
+ <select
5
+ name="content_block[content][language]"
6
+ id="content_language_<%= block.id %>"
7
+ class="np-input"
8
+ data-action="change->np-block-content#save"
9
+ >
10
+ <option value="">Select language</option>
11
+ <% %w[ruby javascript python html css sql bash other].each do |lang| %>
12
+ <option value="<%= lang %>" <%= "selected" if block.content["language"] == lang %>><%= lang.capitalize %></option>
13
+ <% end %>
14
+ </select>
15
+ </div>
16
+ <div class="np-editor-field">
17
+ <label class="np-label" for="content_code_<%= block.id %>">Code</label>
18
+ <textarea
19
+ name="content_block[content][code]"
20
+ id="content_code_<%= block.id %>"
21
+ class="np-input"
22
+ rows="10"
23
+ data-action="blur->np-block-content#save change->np-block-content#save"
24
+ ><%= block.content["code"] %></textarea>
25
+ </div>
26
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="np-editor-divider">
2
+ <p class="np-label">Horizontal Divider</p>
3
+ <hr class="np-divider-preview">
4
+ </div>
@@ -0,0 +1,16 @@
1
+ <div class="np-editor-form">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_form_id_<%= block.id %>">Select Form</label>
4
+ <select
5
+ name="content_block[content][form_id]"
6
+ id="content_form_id_<%= block.id %>"
7
+ class="np-input"
8
+ data-action="change->np-block-content#save"
9
+ >
10
+ <option value="">Choose a form...</option>
11
+ <% NotPressed::Form.active.ordered.each do |form| %>
12
+ <option value="<%= form.id %>" <%= "selected" if block.content["form_id"].to_s == form.id.to_s %>><%= form.name %></option>
13
+ <% end %>
14
+ </select>
15
+ </div>
16
+ </div>
@@ -0,0 +1,75 @@
1
+ <div class="np-editor-gallery">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_columns_<%= block.id %>">Columns</label>
4
+ <select
5
+ name="content_block[content][columns]"
6
+ id="content_columns_<%= block.id %>"
7
+ class="np-input"
8
+ data-action="change->np-block-content#save"
9
+ >
10
+ <% [2, 3, 4].each do |col| %>
11
+ <option value="<%= col %>" <%= "selected" if block.content["columns"].to_i == col %>><%= col %> Columns</option>
12
+ <% end %>
13
+ </select>
14
+ </div>
15
+
16
+ <% images = block.content["images"] || [] %>
17
+ <div class="np-gallery-editor-images">
18
+ <% images.each_with_index do |img, idx| %>
19
+ <div class="np-gallery-editor-item" style="display:flex; gap:0.5rem; align-items:center; margin-bottom:0.5rem; padding:0.5rem; border:1px solid #e2e8f0; border-radius:4px;">
20
+ <div style="flex:1;">
21
+ <label class="np-label">Media ID</label>
22
+ <input
23
+ type="text"
24
+ name="content_block[content][images][][media_id]"
25
+ value="<%= img["media_id"] %>"
26
+ class="np-input"
27
+ placeholder="Media attachment ID"
28
+ data-action="blur->np-block-content#save"
29
+ >
30
+ </div>
31
+ <div style="flex:2;">
32
+ <label class="np-label">Caption</label>
33
+ <input
34
+ type="text"
35
+ name="content_block[content][images][][caption]"
36
+ value="<%= img["caption"] %>"
37
+ class="np-input"
38
+ placeholder="Image caption"
39
+ data-action="blur->np-block-content#save"
40
+ >
41
+ </div>
42
+ </div>
43
+ <% end %>
44
+ </div>
45
+
46
+ <div style="margin-top:0.5rem;">
47
+ <p style="font-size:0.75rem; color:#64748b;">Add images by entering media attachment IDs and captions above. Save after adding each image.</p>
48
+ <% if images.empty? %>
49
+ <div class="np-gallery-editor-item" style="display:flex; gap:0.5rem; align-items:center; margin-bottom:0.5rem; padding:0.5rem; border:1px solid #e2e8f0; border-radius:4px;">
50
+ <div style="flex:1;">
51
+ <label class="np-label">Media ID</label>
52
+ <input
53
+ type="text"
54
+ name="content_block[content][images][][media_id]"
55
+ value=""
56
+ class="np-input"
57
+ placeholder="Media attachment ID"
58
+ data-action="blur->np-block-content#save"
59
+ >
60
+ </div>
61
+ <div style="flex:2;">
62
+ <label class="np-label">Caption</label>
63
+ <input
64
+ type="text"
65
+ name="content_block[content][images][][caption]"
66
+ value=""
67
+ class="np-input"
68
+ placeholder="Image caption"
69
+ data-action="blur->np-block-content#save"
70
+ >
71
+ </div>
72
+ </div>
73
+ <% end %>
74
+ </div>
75
+ </div>
@@ -0,0 +1,26 @@
1
+ <div class="np-editor-heading">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_text_<%= block.id %>">Heading Text</label>
4
+ <input
5
+ type="text"
6
+ name="content_block[content][text]"
7
+ id="content_text_<%= block.id %>"
8
+ value="<%= block.content["text"] %>"
9
+ class="np-input"
10
+ data-action="blur->np-block-content#save change->np-block-content#save"
11
+ >
12
+ </div>
13
+ <div class="np-editor-field">
14
+ <label class="np-label" for="content_level_<%= block.id %>">Level</label>
15
+ <select
16
+ name="content_block[content][level]"
17
+ id="content_level_<%= block.id %>"
18
+ class="np-input"
19
+ data-action="change->np-block-content#save"
20
+ >
21
+ <% (1..6).each do |level| %>
22
+ <option value="<%= level %>" <%= "selected" if block.content["level"].to_i == level %>><%= "H#{level}" %></option>
23
+ <% end %>
24
+ </select>
25
+ </div>
26
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="np-editor-html">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_markup_<%= block.id %>">Raw HTML</label>
4
+ <p class="np-html-warning">Warning: Raw HTML is rendered without sanitization. Use with caution.</p>
5
+ <textarea
6
+ name="content_block[content][markup]"
7
+ id="content_markup_<%= block.id %>"
8
+ class="np-input"
9
+ rows="8"
10
+ data-action="blur->np-block-content#save change->np-block-content#save"
11
+ ><%= block.content["markup"] %></textarea>
12
+ </div>
13
+ </div>
@@ -0,0 +1,56 @@
1
+ <div class="np-editor-image" data-controller="np-media-picker" data-np-media-picker-picker-url-value="<%= picker_admin_media_attachments_path %>">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_url_<%= block.id %>">Image URL</label>
4
+ <input
5
+ type="text"
6
+ name="content_block[content][url]"
7
+ id="content_url_<%= block.id %>"
8
+ value="<%= block.content["url"] %>"
9
+ class="np-input"
10
+ placeholder="https://example.com/image.jpg"
11
+ data-action="blur->np-block-content#save change->np-block-content#save"
12
+ data-np-media-picker-target="urlField"
13
+ >
14
+ </div>
15
+ <input type="hidden" name="content_block[content][media_id]" value="<%= block.content["media_id"] %>" data-np-media-picker-target="mediaIdField">
16
+ <div style="margin-bottom: 0.75rem;">
17
+ <button type="button" class="np-btn np-btn--small" data-action="np-media-picker#openModal">Choose from Library</button>
18
+ </div>
19
+ <% if block.content["url"].present? %>
20
+ <div class="np-image-preview">
21
+ <img src="<%= block.content["url"] %>" alt="<%= block.content["alt"] %>">
22
+ </div>
23
+ <% end %>
24
+ <div class="np-editor-field">
25
+ <label class="np-label" for="content_alt_<%= block.id %>">Alt Text</label>
26
+ <input
27
+ type="text"
28
+ name="content_block[content][alt]"
29
+ id="content_alt_<%= block.id %>"
30
+ value="<%= block.content["alt"] %>"
31
+ class="np-input"
32
+ data-action="blur->np-block-content#save change->np-block-content#save"
33
+ data-np-media-picker-target="altField"
34
+ >
35
+ </div>
36
+ <div class="np-editor-field">
37
+ <label class="np-label" for="content_caption_<%= block.id %>">Caption</label>
38
+ <input
39
+ type="text"
40
+ name="content_block[content][caption]"
41
+ id="content_caption_<%= block.id %>"
42
+ value="<%= block.content["caption"] %>"
43
+ class="np-input"
44
+ data-action="blur->np-block-content#save change->np-block-content#save"
45
+ >
46
+ </div>
47
+ <dialog class="np-modal np-media-picker-dialog" data-np-media-picker-target="dialog" style="width:80%; max-width:900px; max-height:80vh; border:1px solid #e2e8f0; border-radius:8px; padding:0;">
48
+ <div style="display:flex; justify-content:space-between; align-items:center; padding:1rem; border-bottom:1px solid #e2e8f0;">
49
+ <h3 style="margin:0;">Select Image from Media Library</h3>
50
+ <button type="button" class="np-btn np-btn--small" data-action="np-media-picker#closeModal">&times;</button>
51
+ </div>
52
+ <div data-np-media-picker-target="grid" style="overflow-y:auto; max-height:60vh;">
53
+ <p style="text-align:center; padding:2rem; color:#64748b;">Loading...</p>
54
+ </div>
55
+ </dialog>
56
+ </div>
@@ -0,0 +1,24 @@
1
+ <div class="np-editor-quote">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_text_<%= block.id %>">Quote Text</label>
4
+ <textarea
5
+ name="content_block[content][text]"
6
+ id="content_text_<%= block.id %>"
7
+ class="np-input"
8
+ rows="4"
9
+ data-action="blur->np-block-content#save change->np-block-content#save"
10
+ ><%= block.content["text"] %></textarea>
11
+ </div>
12
+ <div class="np-editor-field">
13
+ <label class="np-label" for="content_attribution_<%= block.id %>">Attribution</label>
14
+ <input
15
+ type="text"
16
+ name="content_block[content][attribution]"
17
+ id="content_attribution_<%= block.id %>"
18
+ value="<%= block.content["attribution"] %>"
19
+ class="np-input"
20
+ placeholder="Author name"
21
+ data-action="blur->np-block-content#save change->np-block-content#save"
22
+ >
23
+ </div>
24
+ </div>
@@ -0,0 +1,28 @@
1
+ <div class="np-editor-text" data-controller="np-rich-text">
2
+ <div class="np-editor-field">
3
+ <label class="np-label">Body</label>
4
+ <div class="np-toolbar">
5
+ <button type="button" class="np-toolbar-btn" data-action="click->np-rich-text#bold" title="Bold">B</button>
6
+ <button type="button" class="np-toolbar-btn" data-action="click->np-rich-text#italic" title="Italic"><em>I</em></button>
7
+ <button type="button" class="np-toolbar-btn" data-action="click->np-rich-text#underline" title="Underline"><u>U</u></button>
8
+ <button type="button" class="np-toolbar-btn" data-action="click->np-rich-text#link" title="Insert Link">&#128279;</button>
9
+ <button type="button" class="np-toolbar-btn" data-action="click->np-rich-text#insertUnorderedList" title="Unordered List">&#8226; List</button>
10
+ <button type="button" class="np-toolbar-btn" data-action="click->np-rich-text#insertOrderedList" title="Ordered List">1. List</button>
11
+ </div>
12
+ <div
13
+ class="np-rich-text-editor np-input"
14
+ contenteditable="true"
15
+ data-np-rich-text-target="editor"
16
+ data-action="blur->np-rich-text#sync blur->np-block-content#save"
17
+ ><%= raw block.content["body"] %></div>
18
+ <textarea
19
+ name="content_block[content][body]"
20
+ id="content_body_<%= block.id %>"
21
+ class="np-input"
22
+ rows="8"
23
+ style="display: none;"
24
+ data-np-rich-text-target="textarea"
25
+ data-action="change->np-block-content#save"
26
+ ><%= block.content["body"] %></textarea>
27
+ </div>
28
+ </div>
@@ -0,0 +1,25 @@
1
+ <div class="np-editor-video">
2
+ <div class="np-editor-field">
3
+ <label class="np-label" for="content_url_<%= block.id %>">Video URL</label>
4
+ <input
5
+ type="text"
6
+ name="content_block[content][url]"
7
+ id="content_url_<%= block.id %>"
8
+ value="<%= block.content["url"] %>"
9
+ class="np-input"
10
+ placeholder="https://www.youtube.com/watch?v=..."
11
+ data-action="blur->np-block-content#save change->np-block-content#save"
12
+ >
13
+ </div>
14
+ <div class="np-editor-field">
15
+ <label class="np-label" for="content_caption_<%= block.id %>">Caption</label>
16
+ <input
17
+ type="text"
18
+ name="content_block[content][caption]"
19
+ id="content_caption_<%= block.id %>"
20
+ value="<%= block.content["caption"] %>"
21
+ class="np-input"
22
+ data-action="blur->np-block-content#save change->np-block-content#save"
23
+ >
24
+ </div>
25
+ </div>
@@ -0,0 +1,60 @@
1
+ <h1>Dashboard</h1>
2
+
3
+ <div class="np-stats-grid">
4
+ <div class="np-stat-card">
5
+ <div class="np-stat-number"><%= @total_pages %></div>
6
+ <div class="np-stat-label">Total Pages</div>
7
+ <div class="np-stat-detail"><%= @published_pages %> published, <%= @draft_pages %> draft</div>
8
+ </div>
9
+
10
+ <div class="np-stat-card">
11
+ <div class="np-stat-number"><%= @total_blocks %></div>
12
+ <div class="np-stat-label">Content Blocks</div>
13
+ </div>
14
+
15
+ <div class="np-stat-card">
16
+ <div class="np-stat-number"><%= @total_media %></div>
17
+ <div class="np-stat-label">Media Files</div>
18
+ </div>
19
+
20
+ <div class="np-stat-card">
21
+ <div class="np-stat-number"><%= @total_forms %></div>
22
+ <div class="np-stat-label">Forms</div>
23
+ <div class="np-stat-detail"><%= @active_forms %> active</div>
24
+ </div>
25
+ </div>
26
+
27
+ <h2>Recent Pages</h2>
28
+
29
+ <% if @recent_pages.any? %>
30
+ <table class="np-table">
31
+ <thead>
32
+ <tr>
33
+ <th>Title</th>
34
+ <th>Status</th>
35
+ <th>Content Type</th>
36
+ <th>Updated</th>
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ <% @recent_pages.each do |page| %>
41
+ <tr>
42
+ <td><a href="#"><%= page.title %></a></td>
43
+ <td><span class="np-badge np-badge--<%= page.status %>"><%= page.status %></span></td>
44
+ <td><%= page.content_type %></td>
45
+ <td><%= page.updated_at.strftime("%b %d, %Y %H:%M") %></td>
46
+ </tr>
47
+ <% end %>
48
+ </tbody>
49
+ </table>
50
+ <% else %>
51
+ <p>No pages yet. Create your first page.</p>
52
+ <% end %>
53
+
54
+ <h2>Quick Actions</h2>
55
+
56
+ <div class="np-actions">
57
+ <a href="#" class="np-btn np-btn--primary">New Page</a>
58
+ <a href="/" class="np-btn">View Site</a>
59
+ <a href="#" class="np-btn">Media Library</a>
60
+ </div>
@@ -0,0 +1,33 @@
1
+ <div class="np-field-row" draggable="true" data-np-form-fields-target="item" data-action="dragstart->np-form-fields#dragstart dragover->np-form-fields#dragover dragend->np-form-fields#dragend drop->np-form-fields#drop">
2
+ <%= ff.hidden_field :id %>
3
+ <%= ff.hidden_field :position, data: { np_form_fields_target: "position" } %>
4
+ <%= ff.hidden_field :_destroy, value: "0", data: { np_form_fields_target: "destroy" } %>
5
+
6
+ <span class="np-drag-handle" title="Drag to reorder">&#9776;</span>
7
+
8
+ <div class="np-field-row-inputs">
9
+ <div class="np-field-row-group np-field-row-group--label">
10
+ <%= ff.text_field :label, class: "np-input", placeholder: "Field label" %>
11
+ </div>
12
+
13
+ <div class="np-field-row-group np-field-row-group--type">
14
+ <%= ff.select :field_type, NotPressed::FormField.field_types.keys.map { |t| [t.humanize, t] }, {}, class: "np-input", data: { action: "change->np-form-fields#toggleOptions" } %>
15
+ </div>
16
+
17
+ <div class="np-field-row-group np-field-row-group--required">
18
+ <label class="np-checkbox-label">
19
+ <%= ff.check_box :required %> Required
20
+ </label>
21
+ </div>
22
+
23
+ <div class="np-field-row-group np-field-row-group--placeholder">
24
+ <%= ff.text_field :placeholder, class: "np-input", placeholder: "Placeholder text" %>
25
+ </div>
26
+
27
+ <div class="np-field-row-group np-field-row-group--options" data-np-form-fields-target="optionsGroup" style="<%= ff.object.has_options? ? '' : 'display:none;' %>">
28
+ <%= ff.text_area :options, class: "np-input", rows: 2, placeholder: "Comma-separated options" %>
29
+ </div>
30
+ </div>
31
+
32
+ <button type="button" class="np-btn np-btn--small np-btn--danger" data-action="np-form-fields#removeField" title="Remove field">&times;</button>
33
+ </div>