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,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module NotPressed
6
+ module Generators
7
+ class PluginGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ argument :plugin_name, type: :string, desc: "Name of the plugin (snake_case)"
11
+
12
+ desc "Generate a NotPressed plugin scaffold"
13
+
14
+ def create_plugin_file
15
+ template "plugin.rb.tt",
16
+ "app/plugins/not_pressed/#{plugin_name}_plugin.rb"
17
+ end
18
+
19
+ def show_instructions
20
+ say ""
21
+ say "NotPressed plugin '#{plugin_name}' created!", :green
22
+ say ""
23
+ say "Next steps:"
24
+ say " 1. Edit app/plugins/not_pressed/#{plugin_name}_plugin.rb"
25
+ say " 2. Uncomment and customize the hook, block_type, and settings examples"
26
+ say " 3. The plugin will be auto-discovered and enabled on app boot"
27
+ say ""
28
+ end
29
+
30
+ private
31
+
32
+ def class_name
33
+ plugin_name.camelize
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>Plugin < NotPressed::Plugin
4
+ name "<%= plugin_name %>"
5
+ version "0.1.0"
6
+ description "TODO: Describe what <%= plugin_name %> does"
7
+
8
+ # Hook into CMS events:
9
+ # on(:page_before_save) { |page| ... }
10
+ # on(:page_after_save) { |page| ... }
11
+
12
+ # Register custom block types:
13
+ # block_type :my_block, label: "My Block"
14
+
15
+ # Define plugin settings:
16
+ # settings do
17
+ # string :api_key, label: "API Key", required: true
18
+ # boolean :enabled, label: "Enabled", default: true
19
+ # end
20
+
21
+ # Declare dependencies on other plugins:
22
+ # depends_on "other_plugin"
23
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ module Authentication
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ helper_method :current_admin_user, :admin_user_signed_in?
10
+ end
11
+
12
+ private
13
+
14
+ def authenticate_admin!
15
+ auth = NotPressed.configuration.admin_authentication
16
+ case auth
17
+ when Proc
18
+ instance_exec(&auth)
19
+ when Symbol, String
20
+ send(auth)
21
+ when nil
22
+ # No authentication configured — allow access
23
+ end
24
+ rescue NoMethodError => e
25
+ raise unless e.message.include?(auth.to_s)
26
+
27
+ redirect_to NotPressed.configuration.admin_unauthorized_redirect,
28
+ alert: "Authentication required"
29
+ end
30
+
31
+ def current_admin_user
32
+ user_method = NotPressed.configuration.admin_current_user
33
+ case user_method
34
+ when Proc
35
+ instance_exec(&user_method)
36
+ when Symbol, String
37
+ send(user_method)
38
+ when nil
39
+ nil
40
+ end
41
+ end
42
+
43
+ def admin_user_signed_in?
44
+ current_admin_user.present?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ # Manages the admin sidebar menu items.
6
+ #
7
+ # Supports multiple sections (e.g., +:main+, +:settings+) and positional
8
+ # ordering. Built-in items are protected from {.reset!}.
9
+ module MenuRegistry
10
+ class << self
11
+ # Adds a menu item to the admin sidebar.
12
+ #
13
+ # @param label [String] display text
14
+ # @param path [Symbol, String] route helper name or URL path
15
+ # @param icon [String, nil] icon identifier
16
+ # @param section [Symbol] menu section (default +:main+)
17
+ # @param position [Integer] sort order, lower values appear first (default 100)
18
+ # @param built_in [Boolean] if true, protected from {.reset!} (default false)
19
+ # @return [void]
20
+ def register(label, path:, icon: nil, section: :main, position: 100, built_in: false)
21
+ items_store << {
22
+ label: label,
23
+ path: path,
24
+ icon: icon,
25
+ section: section,
26
+ position: position,
27
+ built_in: built_in
28
+ }
29
+ end
30
+
31
+ # Returns menu items for a section, sorted by position.
32
+ #
33
+ # @param section [Symbol] the section to filter by (default +:main+)
34
+ # @return [Array<Hash>] sorted menu item hashes
35
+ def items(section: :main)
36
+ items_store
37
+ .select { |item| item[:section] == section }
38
+ .sort_by { |item| item[:position] }
39
+ end
40
+
41
+ # Removes all non-built-in menu items.
42
+ #
43
+ # @return [void]
44
+ def reset!
45
+ items_store.reject! { |item| !item[:built_in] }
46
+ end
47
+
48
+ # Removes a menu item matching the given label and path.
49
+ #
50
+ # @param label [String] menu item label
51
+ # @param path [Symbol, String] menu item path
52
+ # @return [void]
53
+ def unregister(label, path:)
54
+ items_store.reject! { |item| item[:label] == label && item[:path] == path }
55
+ end
56
+
57
+ # Removes all menu items including built-ins.
58
+ #
59
+ # @return [void]
60
+ def clear_all!
61
+ @items = []
62
+ end
63
+
64
+ # Registers the default built-in menu items.
65
+ #
66
+ # Idempotent: only executes once unless {.reset_built_ins_flag!} is called.
67
+ #
68
+ # @return [void]
69
+ def register_built_ins!
70
+ return if @built_ins_registered
71
+
72
+ register "Dashboard", path: :admin_root_path, section: :main, position: 0, built_in: true
73
+ register "Pages", path: :admin_pages_path, section: :main, position: 10, built_in: true
74
+ register "Forms", path: :admin_forms_path, section: :main, position: 20, built_in: true
75
+ register "Categories", path: :admin_categories_path, section: :main, position: 25, built_in: true
76
+ register "Tags", path: :admin_tags_path, section: :main, position: 26, built_in: true
77
+ register "Media Library", path: :admin_media_attachments_path, section: :main, position: 30, built_in: true
78
+ register "Code Injection", path: :code_injection_admin_settings_path, section: :settings, position: 0, built_in: true
79
+ register "Themes", path: :admin_themes_path, section: :settings, position: 5, built_in: true
80
+ register "Plugins", path: :admin_plugins_path, section: :settings, position: 10, built_in: true
81
+
82
+ @built_ins_registered = true
83
+ end
84
+
85
+ # Resets the idempotency guard so {.register_built_ins!} can run again.
86
+ #
87
+ # @return [void]
88
+ def reset_built_ins_flag!
89
+ @built_ins_registered = false
90
+ end
91
+
92
+ private
93
+
94
+ def items_store
95
+ @items ||= []
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ # Stores all engine-wide settings. Created automatically by {NotPressed.configuration}.
5
+ #
6
+ # Configure via the block form:
7
+ # NotPressed.configure do |config|
8
+ # config.site_name = "My Blog"
9
+ # config.per_page = 10
10
+ # end
11
+ class Configuration
12
+ # @return [String] URL path prefix for admin routes (default: "not_pressed")
13
+ # @return [String] site display name (default: "My Site")
14
+ # @return [Integer] pagination size (default: 25)
15
+ # @return [Boolean] whether to auto-discover navigable routes (default: true)
16
+ # @return [Boolean] whether to cache navigation trees (default: true)
17
+ # @return [Proc, nil] authentication callback for admin access
18
+ # @return [Proc, nil] callback returning the current admin user
19
+ # @return [String] redirect path when admin auth fails (default: "/")
20
+ # @return [Array<String>] available layout names (default: ["default"])
21
+ # @return [String] separator between page title and site name in SEO tags (default: " | ")
22
+ # @return [String, nil] custom robots.txt content
23
+ # @return [String, nil] HTML code injected into every page head
24
+ # @return [String, nil] HTML code injected into every page body
25
+ # @return [String] name of the default theme (default: "Starter")
26
+ attr_accessor :admin_path, :site_name, :per_page,
27
+ :auto_discover_navigation, :cache_navigation,
28
+ :admin_authentication, :admin_current_user,
29
+ :admin_unauthorized_redirect, :available_layouts,
30
+ :seo_title_separator, :robots_txt,
31
+ :global_head_code, :global_body_code, :default_theme
32
+
33
+ # @return [Hash{String => Navigation::Menu}] DSL-defined navigation menus (read-only)
34
+ attr_reader :menus
35
+
36
+ def initialize
37
+ @admin_path = "not_pressed"
38
+ @site_name = "My Site"
39
+ @per_page = 25
40
+ @auto_discover_navigation = true
41
+ @cache_navigation = true
42
+ @menus = {}
43
+ @admin_authentication = nil
44
+ @admin_current_user = nil
45
+ @admin_unauthorized_redirect = "/"
46
+ @available_layouts = ["default"]
47
+ @seo_title_separator = " | "
48
+ @robots_txt = nil
49
+ @global_head_code = nil
50
+ @global_body_code = nil
51
+ @default_theme = "Starter"
52
+ end
53
+
54
+ # Defines a navigation menu using the {Navigation::Menu} DSL.
55
+ #
56
+ # @param name [Symbol, String] menu identifier
57
+ # @yield DSL block passed to {Navigation::Menu.build}
58
+ # @return [Navigation::Menu]
59
+ def menu(name, &block)
60
+ @menus[name.to_s] = Navigation::Menu.build(name.to_s, &block)
61
+ end
62
+
63
+ # Returns the union of configured layouts and any layouts defined by the active theme.
64
+ #
65
+ # @return [Array<String>] all available layout names
66
+ def effective_layouts
67
+ layouts = @available_layouts.dup
68
+ theme = NotPressed::ThemeRegistry.active
69
+ if theme
70
+ theme.theme_layouts.each do |tl|
71
+ layouts << tl[:name].to_s unless layouts.include?(tl[:name].to_s)
72
+ end
73
+ end
74
+ layouts
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class ContentType
5
+ attr_reader :name, :label, :icon, :description, :allowed_blocks, :has_slug, :has_parent, :custom_fields
6
+
7
+ def initialize(name:, label:, icon: nil, description: nil, allowed_blocks: nil, has_slug: true, has_parent: true, custom_fields: [])
8
+ @name = name.to_s
9
+ @label = label
10
+ @icon = icon
11
+ @description = description
12
+ @allowed_blocks = allowed_blocks
13
+ @has_slug = has_slug
14
+ @has_parent = has_parent
15
+ @custom_fields = custom_fields
16
+ freeze
17
+ end
18
+
19
+ def to_s
20
+ name
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class ContentTypeBuilder
5
+ FieldDefinition = Struct.new(:key, :type, :label, :required, :default, keyword_init: true)
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ @options = {}
10
+ @fields = []
11
+ end
12
+
13
+ def label(val)
14
+ @options[:label] = val
15
+ end
16
+
17
+ def icon(val)
18
+ @options[:icon] = val
19
+ end
20
+
21
+ def description(val)
22
+ @options[:description] = val
23
+ end
24
+
25
+ def allowed_blocks(*types)
26
+ @options[:allowed_blocks] = types.map(&:to_s)
27
+ end
28
+
29
+ def has_slug(val)
30
+ @options[:has_slug] = val
31
+ end
32
+
33
+ def has_parent(val)
34
+ @options[:has_parent] = val
35
+ end
36
+
37
+ def field(key, type, **options)
38
+ @fields << FieldDefinition.new(
39
+ key: key,
40
+ type: type,
41
+ label: options[:label],
42
+ required: options.fetch(:required, false),
43
+ default: options[:default]
44
+ )
45
+ end
46
+
47
+ def register!
48
+ ContentTypeRegistry.register(@name, **@options, custom_fields: @fields)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class ContentTypeRegistry
5
+ class NotFound < StandardError; end
6
+
7
+ class << self
8
+ def registry
9
+ @registry ||= {}
10
+ end
11
+
12
+ def register(name, **options)
13
+ type = ContentType.new(name: name, **options)
14
+ registry[type.name] = type
15
+ end
16
+
17
+ def find(name)
18
+ registry[name.to_s] || raise(NotFound, "Content type '#{name}' not found")
19
+ end
20
+
21
+ def all
22
+ registry.values
23
+ end
24
+
25
+ def names
26
+ registry.keys
27
+ end
28
+
29
+ def reset!
30
+ @registry = {}
31
+ register_defaults
32
+ end
33
+
34
+ private
35
+
36
+ def register_defaults
37
+ register("page", label: "Page", icon: "file-text", description: "A standard content page")
38
+ register("blog_post", label: "Blog Post", icon: "pen-line", description: "A blog post entry")
39
+ register("gallery", label: "Gallery", icon: "images", description: "An image gallery page")
40
+ end
41
+ end
42
+
43
+ register_defaults
44
+ end
45
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace NotPressed
6
+
7
+ config.generators do |g|
8
+ g.orm :active_record
9
+ g.test_framework :rspec
10
+ end
11
+
12
+ initializer "not_pressed.mime_types" do
13
+ Mime::Type.register "text/vnd.turbo-stream.html", :turbo_stream unless Mime::Type.lookup_by_extension(:turbo_stream)
14
+ end
15
+
16
+ initializer "not_pressed.assets" do |app|
17
+ if app.config.respond_to?(:assets)
18
+ app.config.assets.precompile += %w[not_pressed/application.css not_pressed/admin.css not_pressed/themes/starter.css not_pressed/gallery.css not_pressed/lightbox.js]
19
+ end
20
+ end
21
+
22
+ initializer "not_pressed.active_storage_check" do
23
+ unless defined?(ActiveStorage)
24
+ Rails.logger.warn(
25
+ "NotPressed: Active Storage is not loaded. " \
26
+ "Media attachments require Active Storage. " \
27
+ "Add `require \"active_storage/engine\"` to your application.rb."
28
+ )
29
+ end
30
+ end
31
+
32
+ initializer "not_pressed.gallery_block" do
33
+ config.after_initialize do
34
+ NotPressed::ContentBlock.register_type("gallery")
35
+
36
+ gallery_renderer = lambda do |content, _block|
37
+ images = content["images"] || []
38
+ columns = content["columns"].to_i
39
+ columns = 3 unless (2..4).cover?(columns)
40
+
41
+ if images.empty?
42
+ return %(<div class="np-content-gallery" data-columns="#{columns}"></div>)
43
+ end
44
+
45
+ items_html = images.map do |img|
46
+ media_id = img["media_id"]
47
+ caption = ERB::Util.html_escape(img["caption"].to_s)
48
+ url = ""
49
+
50
+ if media_id.present?
51
+ media = NotPressed::MediaAttachment.find_by(id: media_id)
52
+ if media&.file&.attached?
53
+ url = Rails.application.routes.url_helpers.rails_blob_path(media.file, disposition: "inline", only_path: true)
54
+ end
55
+ end
56
+
57
+ item_html = %(<div class="np-gallery-item">)
58
+ item_html += %(<img src="#{ERB::Util.html_escape(url)}" alt="#{caption}" loading="lazy">) unless url.empty?
59
+ item_html += %(<div class="np-gallery-caption">#{caption}</div>) unless caption.empty?
60
+ item_html += %(</div>)
61
+ item_html
62
+ end.join("\n")
63
+
64
+ %(<div class="np-content-gallery" data-columns="#{columns}" style="--np-gallery-columns: #{columns};">#{items_html}</div>)
65
+ end
66
+
67
+ NotPressed::Rendering::RendererRegistry.register("gallery", gallery_renderer)
68
+ NotPressed::Rendering::RendererRegistry.register_editor("gallery", "not_pressed/admin/content_blocks/editors/gallery")
69
+ NotPressed::Rendering::RendererRegistry.register_default_content("gallery", { "images" => [], "columns" => 3 })
70
+ end
71
+ end
72
+
73
+ initializer "not_pressed.admin_menu" do
74
+ NotPressed::Admin::MenuRegistry.register_built_ins!
75
+ end
76
+
77
+ initializer "not_pressed.theme_autoload", before: :set_autoload_paths do |app|
78
+ # Engine's own themes
79
+ engine_themes = root.join("app", "themes")
80
+ if engine_themes.exist?
81
+ app.config.autoload_paths << engine_themes.to_s
82
+ end
83
+
84
+ # Host app themes
85
+ themes_path = app.root.join("app", "themes")
86
+ if themes_path.exist?
87
+ app.config.autoload_paths << themes_path.to_s
88
+ end
89
+ end
90
+
91
+ initializer "not_pressed.plugin_autoload" do |app|
92
+ plugins_path = app.root.join("app", "plugins")
93
+ if plugins_path.exist?
94
+ app.config.autoload_paths << plugins_path.to_s
95
+ end
96
+ end
97
+
98
+ initializer "not_pressed.themes" do
99
+ config.after_initialize do
100
+ # Discover and register all Theme subclasses
101
+ NotPressed::Theme.subclasses.each do |klass|
102
+ next unless klass.plugin_name
103
+ next if NotPressed::ThemeRegistry.available.include?(klass.plugin_name)
104
+ NotPressed::ThemeRegistry.register(klass)
105
+ end
106
+
107
+ # Auto-activate default theme if none active
108
+ default_theme = NotPressed.configuration.default_theme
109
+ if default_theme && NotPressed::ThemeRegistry.active.nil?
110
+ begin
111
+ NotPressed::ThemeRegistry.activate(default_theme)
112
+ rescue NotPressed::ThemeRegistry::NotFound
113
+ # Default theme not available, no action needed
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ initializer "not_pressed.plugins" do
120
+ config.after_initialize do
121
+ NotPressed::PluginManager.discover
122
+ NotPressed::PluginManager.enable_all
123
+ end
124
+ end
125
+
126
+ initializer "not_pressed.append_migrations" do |app|
127
+ config.paths["db/migrate"].expanded.each do |expanded_path|
128
+ app.config.paths["db/migrate"] << expanded_path unless app.config.paths["db/migrate"].include?(expanded_path)
129
+ end
130
+ end
131
+ end
132
+ end