plum-cms 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 (178) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +20 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +252 -0
  5. data/app/assets/builds/tailwind.css +2 -0
  6. data/app/assets/images/plum-mark.svg +7 -0
  7. data/app/assets/images/table-needs-logo.svg +83 -0
  8. data/app/assets/javascripts/plum/application.js +15 -0
  9. data/app/assets/stylesheets/application.css +10 -0
  10. data/app/assets/stylesheets/plum/control_panel.css +2 -0
  11. data/app/assets/tailwind/application.css +47 -0
  12. data/app/controllers/plum/application_controller.rb +34 -0
  13. data/app/controllers/plum/cp/assets_controller.rb +84 -0
  14. data/app/controllers/plum/cp/base_controller.rb +45 -0
  15. data/app/controllers/plum/cp/content_types_controller.rb +59 -0
  16. data/app/controllers/plum/cp/dashboard_controller.rb +10 -0
  17. data/app/controllers/plum/cp/entries_controller.rb +240 -0
  18. data/app/controllers/plum/cp/form_definitions_controller.rb +105 -0
  19. data/app/controllers/plum/cp/form_submissions_controller.rb +26 -0
  20. data/app/controllers/plum/cp/globals_controller.rb +84 -0
  21. data/app/controllers/plum/cp/nav_items_controller.rb +68 -0
  22. data/app/controllers/plum/cp/nav_menus_controller.rb +56 -0
  23. data/app/controllers/plum/cp/site_settings_controller.rb +87 -0
  24. data/app/controllers/plum/cp/taxonomies_controller.rb +56 -0
  25. data/app/controllers/plum/cp/terms_controller.rb +55 -0
  26. data/app/controllers/plum/cp/theme_previews_controller.rb +27 -0
  27. data/app/controllers/plum/cp/themes_controller.rb +98 -0
  28. data/app/controllers/plum/form_submissions_controller.rb +44 -0
  29. data/app/controllers/plum/pages_controller.rb +161 -0
  30. data/app/controllers/plum/sessions_controller.rb +25 -0
  31. data/app/controllers/plum/theme_assets_controller.rb +14 -0
  32. data/app/helpers/application_helper.rb +2 -0
  33. data/app/helpers/markdown_helper.rb +21 -0
  34. data/app/helpers/plum/cp/assets_helper.rb +21 -0
  35. data/app/helpers/plum/cp/theme_settings_helper.rb +26 -0
  36. data/app/javascript/application.js +18 -0
  37. data/app/javascript/controllers/application.js +9 -0
  38. data/app/javascript/controllers/index.js +4 -0
  39. data/app/javascript/controllers/plum/blocks_editor_controller.js +275 -0
  40. data/app/javascript/controllers/plum/blueprint_controller.js +120 -0
  41. data/app/javascript/controllers/plum/form_fields_controller.js +84 -0
  42. data/app/javascript/controllers/plum/image_picker_controller.js +143 -0
  43. data/app/javascript/controllers/plum/theme_settings_controller.js +17 -0
  44. data/app/models/plum/application_record.rb +5 -0
  45. data/app/models/plum/asset.rb +69 -0
  46. data/app/models/plum/content_type.rb +35 -0
  47. data/app/models/plum/entry.rb +67 -0
  48. data/app/models/plum/entry_term.rb +8 -0
  49. data/app/models/plum/form_definition.rb +68 -0
  50. data/app/models/plum/form_submission.rb +76 -0
  51. data/app/models/plum/global.rb +24 -0
  52. data/app/models/plum/nav_item.rb +58 -0
  53. data/app/models/plum/nav_menu.rb +24 -0
  54. data/app/models/plum/site.rb +112 -0
  55. data/app/models/plum/site_scoped.rb +17 -0
  56. data/app/models/plum/site_setting.rb +35 -0
  57. data/app/models/plum/taxonomy.rb +24 -0
  58. data/app/models/plum/term.rb +23 -0
  59. data/app/models/plum/user.rb +16 -0
  60. data/app/services/plum/base_blocks.rb +161 -0
  61. data/app/services/plum/block_editor_config.rb +65 -0
  62. data/app/services/plum/block_library.rb +31 -0
  63. data/app/services/plum/builder_renderer.rb +63 -0
  64. data/app/services/plum/field_expander.rb +78 -0
  65. data/app/services/plum/form_renderer.rb +110 -0
  66. data/app/services/plum/liquid_context.rb +335 -0
  67. data/app/services/plum/liquid_filters.rb +58 -0
  68. data/app/services/plum/liquid_renderer.rb +49 -0
  69. data/app/services/plum/theme.rb +160 -0
  70. data/app/services/plum/theme_asset_path.rb +31 -0
  71. data/app/services/plum/theme_package_installer.rb +287 -0
  72. data/app/services/plum/theme_registry.rb +63 -0
  73. data/app/services/plum/theme_resolver.rb +55 -0
  74. data/app/services/plum/theme_settings_params.rb +54 -0
  75. data/app/themes/bagel-boy/assets/screenshot.svg +92 -0
  76. data/app/themes/bagel-boy/assets/theme.css +184 -0
  77. data/app/themes/bagel-boy/blocks/hours.liquid +4 -0
  78. data/app/themes/bagel-boy/blocks/menu_item.liquid +7 -0
  79. data/app/themes/bagel-boy/layouts/base.liquid +64 -0
  80. data/app/themes/bagel-boy/templates/collections/posts.liquid +32 -0
  81. data/app/themes/bagel-boy/templates/entries/_default.liquid +13 -0
  82. data/app/themes/bagel-boy/templates/entries/landing.liquid +4 -0
  83. data/app/themes/bagel-boy/templates/entries/pages.liquid +18 -0
  84. data/app/themes/bagel-boy/templates/entries/posts.liquid +8 -0
  85. data/app/themes/bagel-boy/templates/index.liquid +10 -0
  86. data/app/themes/bagel-boy/templates/search.liquid +28 -0
  87. data/app/themes/bagel-boy/theme.yml +47 -0
  88. data/app/themes/bagel-shop/assets/screenshot.svg +28 -0
  89. data/app/themes/bagel-shop/assets/theme.css +7 -0
  90. data/app/themes/bagel-shop/layouts/base.liquid +507 -0
  91. data/app/themes/bagel-shop/templates/entries/_default.liquid +15 -0
  92. data/app/themes/bagel-shop/templates/entries/pages.liquid +12 -0
  93. data/app/themes/bagel-shop/templates/entries/posts.liquid +19 -0
  94. data/app/themes/bagel-shop/templates/index.liquid +55 -0
  95. data/app/themes/bagel-shop/theme.yml +30 -0
  96. data/app/themes/default/assets/screenshot.svg +24 -0
  97. data/app/themes/default/assets/theme.css +89 -0
  98. data/app/themes/default/layouts/base.liquid +117 -0
  99. data/app/themes/default/templates/collections/_default.liquid +16 -0
  100. data/app/themes/default/templates/collections/posts.liquid +27 -0
  101. data/app/themes/default/templates/entries/_default.liquid +26 -0
  102. data/app/themes/default/templates/entries/landing.liquid +5 -0
  103. data/app/themes/default/templates/entries/pages.liquid +17 -0
  104. data/app/themes/default/templates/entries/posts.liquid +20 -0
  105. data/app/themes/default/templates/index.liquid +22 -0
  106. data/app/themes/default/templates/search.liquid +28 -0
  107. data/app/themes/default/templates/taxonomies/_default.liquid +25 -0
  108. data/app/themes/default/templates/taxonomies/_default_index.liquid +14 -0
  109. data/app/themes/default/theme.yml +23 -0
  110. data/app/views/layouts/plum/cp.html.erb +138 -0
  111. data/app/views/layouts/plum/session.html.erb +43 -0
  112. data/app/views/plum/cp/assets/_form.html.erb +44 -0
  113. data/app/views/plum/cp/assets/edit.html.erb +14 -0
  114. data/app/views/plum/cp/assets/index.html.erb +52 -0
  115. data/app/views/plum/cp/assets/new.html.erb +9 -0
  116. data/app/views/plum/cp/content_types/_form.html.erb +113 -0
  117. data/app/views/plum/cp/content_types/edit.html.erb +8 -0
  118. data/app/views/plum/cp/content_types/index.html.erb +48 -0
  119. data/app/views/plum/cp/content_types/new.html.erb +5 -0
  120. data/app/views/plum/cp/content_types/show.html.erb +59 -0
  121. data/app/views/plum/cp/dashboard/show.html.erb +50 -0
  122. data/app/views/plum/cp/entries/_form.html.erb +196 -0
  123. data/app/views/plum/cp/entries/edit.html.erb +9 -0
  124. data/app/views/plum/cp/entries/index.html.erb +51 -0
  125. data/app/views/plum/cp/entries/new.html.erb +5 -0
  126. data/app/views/plum/cp/form_definitions/_form.html.erb +90 -0
  127. data/app/views/plum/cp/form_definitions/edit.html.erb +11 -0
  128. data/app/views/plum/cp/form_definitions/index.html.erb +43 -0
  129. data/app/views/plum/cp/form_definitions/new.html.erb +5 -0
  130. data/app/views/plum/cp/form_definitions/show.html.erb +70 -0
  131. data/app/views/plum/cp/form_submissions/show.html.erb +20 -0
  132. data/app/views/plum/cp/globals/_form.html.erb +44 -0
  133. data/app/views/plum/cp/globals/edit.html.erb +11 -0
  134. data/app/views/plum/cp/globals/index.html.erb +41 -0
  135. data/app/views/plum/cp/globals/new.html.erb +5 -0
  136. data/app/views/plum/cp/globals/show.html.erb +14 -0
  137. data/app/views/plum/cp/nav_items/_form.html.erb +56 -0
  138. data/app/views/plum/cp/nav_items/edit.html.erb +11 -0
  139. data/app/views/plum/cp/nav_items/new.html.erb +6 -0
  140. data/app/views/plum/cp/nav_menus/_form.html.erb +33 -0
  141. data/app/views/plum/cp/nav_menus/_nav_item.html.erb +24 -0
  142. data/app/views/plum/cp/nav_menus/edit.html.erb +11 -0
  143. data/app/views/plum/cp/nav_menus/index.html.erb +41 -0
  144. data/app/views/plum/cp/nav_menus/new.html.erb +5 -0
  145. data/app/views/plum/cp/nav_menus/show.html.erb +24 -0
  146. data/app/views/plum/cp/shared/_icon.html.erb +39 -0
  147. data/app/views/plum/cp/shared/_image_picker.html.erb +38 -0
  148. data/app/views/plum/cp/site_settings/edit.html.erb +145 -0
  149. data/app/views/plum/cp/site_settings/show.html.erb +62 -0
  150. data/app/views/plum/cp/taxonomies/_form.html.erb +38 -0
  151. data/app/views/plum/cp/taxonomies/edit.html.erb +2 -0
  152. data/app/views/plum/cp/taxonomies/index.html.erb +38 -0
  153. data/app/views/plum/cp/taxonomies/new.html.erb +2 -0
  154. data/app/views/plum/cp/taxonomies/show.html.erb +46 -0
  155. data/app/views/plum/cp/terms/_form.html.erb +30 -0
  156. data/app/views/plum/cp/terms/edit.html.erb +2 -0
  157. data/app/views/plum/cp/terms/new.html.erb +2 -0
  158. data/app/views/plum/cp/themes/index.html.erb +160 -0
  159. data/app/views/plum/form_mailer/submission_notification.html.erb +13 -0
  160. data/app/views/plum/form_mailer/submission_notification.text.erb +7 -0
  161. data/app/views/plum/sessions/new.html.erb +20 -0
  162. data/compat/plum-cms.rb +1 -0
  163. data/config/locales/en.yml +31 -0
  164. data/config/plum_importmap.rb +4 -0
  165. data/config/plum_routes.rb +34 -0
  166. data/db/engine_migrate/20260101000001_create_plum_tables.rb +154 -0
  167. data/lib/generators/plum/install/install_generator.rb +54 -0
  168. data/lib/generators/plum/install/templates/plum_initializer.rb +37 -0
  169. data/lib/plum/configuration.rb +88 -0
  170. data/lib/plum/content_source.rb +45 -0
  171. data/lib/plum/content_source_context.rb +30 -0
  172. data/lib/plum/content_source_registry.rb +120 -0
  173. data/lib/plum/engine.rb +30 -0
  174. data/lib/plum/liquid_tags/form_tag.rb +17 -0
  175. data/lib/plum/version.rb +7 -0
  176. data/lib/plum.rb +11 -0
  177. data/vendor/javascript/lexxy.js +11859 -0
  178. metadata +405 -0
@@ -0,0 +1,160 @@
1
+ <div class="flex items-center justify-between gap-6 mb-8">
2
+ <div>
3
+ <h1 class="text-2xl font-bold text-gray-900">Themes</h1>
4
+ <p class="mt-1 text-sm text-gray-500">
5
+ Active: <span class="font-medium text-gray-700"><%= @active_theme&.name || "Default" %></span>
6
+ </p>
7
+ </div>
8
+ </div>
9
+
10
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
11
+ <section class="lg:col-span-2">
12
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
13
+ <% @themes.each do |theme| %>
14
+ <% active = theme.handle == @active_theme&.handle %>
15
+ <% screenshot_url = theme_screenshot_url(theme) %>
16
+
17
+ <article class="overflow-hidden bg-white shadow rounded-lg" data-theme-handle="<%= theme.handle %>">
18
+ <div class="border-b border-gray-200 bg-gray-100">
19
+ <% if screenshot_url.present? %>
20
+ <%= image_tag screenshot_url,
21
+ alt: "#{theme.name} screenshot",
22
+ class: "h-44 w-full object-cover" %>
23
+ <% else %>
24
+ <div class="flex h-44 w-full items-center justify-center bg-gray-100 text-gray-500">
25
+ <span class="text-3xl font-bold"><%= theme_initials(theme) %></span>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+
30
+ <div class="p-6 space-y-4">
31
+ <div class="flex items-start justify-between gap-4">
32
+ <div class="min-w-0">
33
+ <h2 class="text-lg font-semibold text-gray-900"><%= theme.name %></h2>
34
+ <p class="mt-1 text-sm text-gray-500"><%= theme.description.presence || "No description provided." %></p>
35
+ </div>
36
+
37
+ <% if active %>
38
+ <span class="shrink-0 px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span>
39
+ <% end %>
40
+ </div>
41
+
42
+ <dl class="grid grid-cols-2 gap-3 text-xs text-gray-500">
43
+ <div>
44
+ <dt class="font-medium text-gray-700">Handle</dt>
45
+ <dd class="mt-1 font-mono truncate"><%= theme.handle %></dd>
46
+ </div>
47
+ <div>
48
+ <dt class="font-medium text-gray-700">Version</dt>
49
+ <dd class="mt-1"><%= theme.version.presence || "—" %></dd>
50
+ </div>
51
+ <div>
52
+ <dt class="font-medium text-gray-700">Category</dt>
53
+ <dd class="mt-1"><%= theme.category.presence || "General" %></dd>
54
+ </div>
55
+ <div>
56
+ <dt class="font-medium text-gray-700">Author</dt>
57
+ <dd class="mt-1 truncate"><%= theme.author.presence || "—" %></dd>
58
+ </div>
59
+ </dl>
60
+
61
+ <div class="flex flex-wrap items-center gap-2">
62
+ <%= link_to "Preview", cp_theme_preview_path(theme.handle), target: "_blank", rel: "noopener",
63
+ class: "px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
64
+
65
+ <% unless active %>
66
+ <%= button_to "Activate", cp_theme_path(theme.handle), method: :patch,
67
+ class: "px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 cursor-pointer" %>
68
+ <% end %>
69
+ </div>
70
+ </div>
71
+ </article>
72
+ <% end %>
73
+ </div>
74
+ </section>
75
+
76
+ <aside class="space-y-6">
77
+ <section class="bg-white shadow rounded-lg p-6 space-y-4">
78
+ <div>
79
+ <h2 class="text-lg font-semibold text-gray-900">Customize</h2>
80
+ <p class="mt-1 text-sm text-gray-500"><%= @active_theme&.name || "Default" %></p>
81
+ </div>
82
+
83
+ <% if @active_theme&.settings_fields&.any? %>
84
+ <%= form_with url: cp_theme_path(@active_theme.handle), method: :patch, class: "space-y-4" do %>
85
+ <% @active_theme.settings_fields.each do |field| %>
86
+ <% field_id = theme_setting_input_id(@active_theme, field) %>
87
+ <% field_name = theme_setting_input_name(field) %>
88
+ <% field_value = theme_setting_value(field) %>
89
+
90
+ <div>
91
+ <label for="<%= field_id %>" class="block text-sm font-medium text-gray-700"><%= theme_setting_label(field) %></label>
92
+
93
+ <% case field["type"].to_s %>
94
+ <% when "color" %>
95
+ <%= color_field_tag field_name, field_value,
96
+ id: field_id,
97
+ class: "mt-1 h-10 w-16 rounded-md border border-gray-300 bg-white p-1" %>
98
+ <% when "textarea" %>
99
+ <%= text_area_tag field_name, field_value, id: field_id, rows: 3,
100
+ class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
101
+ <% when "boolean" %>
102
+ <div class="mt-2 flex items-center gap-2">
103
+ <%= hidden_field_tag field_name, "0" %>
104
+ <%= check_box_tag field_name, "1", theme_setting_checked?(field),
105
+ id: field_id,
106
+ class: "h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" %>
107
+ </div>
108
+ <% when "select" %>
109
+ <%= select_tag field_name,
110
+ options_for_select(theme_setting_options(field), field_value),
111
+ id: field_id,
112
+ class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
113
+ <% else %>
114
+ <%= text_field_tag field_name, field_value, id: field_id,
115
+ class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
116
+ <% end %>
117
+
118
+ <% if field["help"].present? %>
119
+ <p class="mt-1 text-sm text-gray-500"><%= field["help"] %></p>
120
+ <% end %>
121
+ </div>
122
+ <% end %>
123
+
124
+ <div class="flex items-center justify-between gap-3 pt-2">
125
+ <%= link_to "Preview", cp_theme_preview_path(@active_theme.handle), target: "_blank", rel: "noopener",
126
+ class: "px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
127
+ <%= submit_tag "Save Settings",
128
+ class: "px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 cursor-pointer" %>
129
+ </div>
130
+ <% end %>
131
+ <% else %>
132
+ <p class="text-sm text-gray-500">This theme does not define editable settings.</p>
133
+ <% end %>
134
+ </section>
135
+
136
+ <section class="bg-white shadow rounded-lg p-6 space-y-4">
137
+ <h2 class="text-lg font-semibold text-gray-900">Install Theme</h2>
138
+
139
+ <% if @theme_package_errors.present? %>
140
+ <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
141
+ <ul class="list-disc list-inside">
142
+ <% @theme_package_errors.each do |error| %>
143
+ <li><%= error %></li>
144
+ <% end %>
145
+ </ul>
146
+ </div>
147
+ <% end %>
148
+
149
+ <%= form_with url: cp_themes_path, multipart: true, class: "space-y-4" do |f| %>
150
+ <div>
151
+ <%= f.label :theme_package, "Theme zip", class: "block text-sm font-medium text-gray-700" %>
152
+ <%= f.file_field :theme_package, accept: ".zip,application/zip", class: "mt-1 block w-full text-sm text-gray-700" %>
153
+ </div>
154
+
155
+ <%= f.submit "Install Theme",
156
+ class: "w-full px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 cursor-pointer" %>
157
+ <% end %>
158
+ </section>
159
+ </aside>
160
+ </div>
@@ -0,0 +1,13 @@
1
+ <h1>New <%= @form.name %> submission</h1>
2
+
3
+ <p>A new submission was received<%= " on #{@site_name}" if @site_name.present? %>.</p>
4
+
5
+ <table cellpadding="6" cellspacing="0" border="0">
6
+ <% @fields.each do |field| %>
7
+ <% handle = field["handle"].to_s %>
8
+ <tr>
9
+ <th align="left" valign="top"><%= field["label"].presence || handle.humanize %></th>
10
+ <td><%= @submission.value(handle) %></td>
11
+ </tr>
12
+ <% end %>
13
+ </table>
@@ -0,0 +1,7 @@
1
+ New <%= @form.name %> submission
2
+ <%= "Received on #{@site_name}" if @site_name.present? %>
3
+
4
+ <% @fields.each do |field| %>
5
+ <% handle = field["handle"].to_s %>
6
+ <%= field["label"].presence || handle.humanize %>: <%= @submission.value(handle) %>
7
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <%= form_with url: "/login", class: "mt-8 space-y-6" do |f| %>
2
+ <div class="space-y-4">
3
+ <div>
4
+ <%= f.label :email, class: "block text-sm font-medium text-gray-700" %>
5
+ <%= f.email_field :email, required: true, autofocus: true,
6
+ class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
7
+ </div>
8
+
9
+ <div>
10
+ <%= f.label :password, class: "block text-sm font-medium text-gray-700" %>
11
+ <%= f.password_field :password, required: true,
12
+ class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
13
+ </div>
14
+ </div>
15
+
16
+ <div>
17
+ <%= f.submit "Sign in",
18
+ class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 cursor-pointer" %>
19
+ </div>
20
+ <% end %>
@@ -0,0 +1 @@
1
+ require "plum"
@@ -0,0 +1,31 @@
1
+ # Files in the config/locales directory are used for internationalization and
2
+ # are automatically loaded by Rails. If you want to use locales other than
3
+ # English, add the necessary files in this directory.
4
+ #
5
+ # To use the locales, use `I18n.t`:
6
+ #
7
+ # I18n.t "hello"
8
+ #
9
+ # In views, this is aliased to just `t`:
10
+ #
11
+ # <%= t("hello") %>
12
+ #
13
+ # To use a different locale, set it with `I18n.locale`:
14
+ #
15
+ # I18n.locale = :es
16
+ #
17
+ # This would use the information in config/locales/es.yml.
18
+ #
19
+ # To learn more about the API, please read the Rails Internationalization guide
20
+ # at https://guides.rubyonrails.org/i18n.html.
21
+ #
22
+ # Be aware that YAML interprets the following case-insensitive strings as
23
+ # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
24
+ # must be quoted to be interpreted as strings. For example:
25
+ #
26
+ # en:
27
+ # "yes": yup
28
+ # enabled: "ON"
29
+
30
+ en:
31
+ hello: "Hello world"
@@ -0,0 +1,4 @@
1
+ pin "plum/application", to: "plum/application.js"
2
+ pin_all_from Plum::Engine.root.join("app/javascript/controllers/plum"), under: "controllers/plum", to: "plum"
3
+ pin "lexxy", to: "lexxy.js"
4
+ pin "@rails/activestorage", to: "activestorage.esm.js"
@@ -0,0 +1,34 @@
1
+ Plum::Engine.routes.draw do
2
+ get "login", to: "sessions#new"
3
+ post "login", to: "sessions#create"
4
+ delete "logout", to: "sessions#destroy"
5
+
6
+ namespace :cp do
7
+ root "dashboard#show"
8
+ resources :content_types do
9
+ resources :entries
10
+ end
11
+ resources :assets, except: [ :show ]
12
+ resources :globals
13
+ resources :nav_menus do
14
+ resources :nav_items, except: [ :index, :show ]
15
+ end
16
+ resources :form_definitions, path: "forms" do
17
+ resources :form_submissions, only: [ :show, :destroy ]
18
+ end
19
+ resource :site_settings, only: [ :show, :edit, :update ]
20
+ resources :themes, only: [ :index, :create, :update ]
21
+ resources :taxonomies do
22
+ resources :terms, except: [ :index, :show ]
23
+ end
24
+ get "theme_previews/:handle", to: "theme_previews#show", as: :theme_preview
25
+ end
26
+
27
+ post "forms/:handle", to: "form_submissions#create", as: :form
28
+ get "theme_assets/:theme_handle/*path", to: "theme_assets#show", as: :theme_asset, format: false
29
+
30
+ get "search", to: "pages#search", as: :search
31
+
32
+ root "pages#home"
33
+ get "*slug", to: "pages#show", constraints: ->(req) { !req.path.start_with?("/rails/") }
34
+ end
@@ -0,0 +1,154 @@
1
+ class CreatePlumTables < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :plum_sites do |t|
4
+ t.string :name, null: false
5
+ t.string :domain
6
+ t.string :theme_name, null: false, default: "default"
7
+ t.json :settings
8
+ t.json :theme_settings, default: {}, null: false
9
+ t.text :custom_css
10
+ t.string :owner_type
11
+ t.bigint :owner_id
12
+ t.timestamps
13
+
14
+ t.index [ :owner_type, :owner_id ], unique: true, name: "index_plum_sites_on_owner"
15
+ end
16
+
17
+ create_table :plum_users do |t|
18
+ t.string :email
19
+ t.string :password_digest
20
+ t.integer :role
21
+ t.timestamps
22
+
23
+ t.index :email, unique: true
24
+ end
25
+
26
+ create_table :plum_site_settings do |t|
27
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }, index: { unique: true }
28
+ t.string :name
29
+ t.string :tagline
30
+ t.string :logo
31
+ t.string :favicon
32
+ t.string :seo_title
33
+ t.string :seo_description
34
+ t.string :theme_name
35
+ t.string :primary_color
36
+ t.string :support_email
37
+ t.timestamps
38
+ end
39
+
40
+ create_table :plum_content_types do |t|
41
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
42
+ t.string :name
43
+ t.string :handle
44
+ t.boolean :singleton
45
+ t.json :blueprint
46
+ t.string :icon
47
+ t.timestamps
48
+
49
+ t.index [ :site_id, :handle ], unique: true
50
+ end
51
+
52
+ create_table :plum_entries do |t|
53
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
54
+ t.references :content_type, null: false, foreign_key: { to_table: :plum_content_types }
55
+ t.references :author, foreign_key: { to_table: :plum_users }
56
+ t.string :author_name
57
+ t.string :author_email
58
+ t.string :author_gid
59
+ t.string :title
60
+ t.string :slug
61
+ t.integer :status
62
+ t.json :data
63
+ t.datetime :published_at
64
+ t.timestamps
65
+
66
+ t.index [ :site_id, :slug ], unique: true
67
+ end
68
+
69
+ create_table :plum_globals do |t|
70
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
71
+ t.string :name
72
+ t.string :handle
73
+ t.json :data
74
+ t.timestamps
75
+
76
+ t.index [ :site_id, :handle ], unique: true
77
+ end
78
+
79
+ create_table :plum_nav_menus do |t|
80
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
81
+ t.string :name
82
+ t.string :handle
83
+ t.timestamps
84
+
85
+ t.index [ :site_id, :handle ], unique: true
86
+ end
87
+
88
+ create_table :plum_nav_items do |t|
89
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
90
+ t.references :nav_menu, null: false, foreign_key: { to_table: :plum_nav_menus }
91
+ t.references :parent, foreign_key: { to_table: :plum_nav_items }
92
+ t.references :entry, foreign_key: { to_table: :plum_entries }
93
+ t.string :label
94
+ t.string :url
95
+ t.integer :position
96
+ t.timestamps
97
+ end
98
+
99
+ create_table :plum_assets do |t|
100
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
101
+ t.string :alt_text
102
+ t.text :caption
103
+ t.string :folder
104
+ t.timestamps
105
+ end
106
+
107
+ create_table :plum_form_definitions do |t|
108
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
109
+ t.string :name
110
+ t.string :handle
111
+ t.json :fields
112
+ t.string :notification_email
113
+ t.timestamps
114
+
115
+ t.index [ :site_id, :handle ], unique: true
116
+ end
117
+
118
+ create_table :plum_form_submissions do |t|
119
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
120
+ t.references :form_definition, null: false, foreign_key: { to_table: :plum_form_definitions }
121
+ t.json :data
122
+ t.timestamps
123
+ end
124
+
125
+ create_table :plum_taxonomies do |t|
126
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
127
+ t.string :name, null: false
128
+ t.string :handle, null: false
129
+ t.string :slug, null: false
130
+ t.timestamps
131
+
132
+ t.index [ :site_id, :handle ], unique: true
133
+ t.index [ :site_id, :slug ], unique: true
134
+ end
135
+
136
+ create_table :plum_terms do |t|
137
+ t.references :site, null: false, foreign_key: { to_table: :plum_sites }
138
+ t.references :taxonomy, null: false, foreign_key: { to_table: :plum_taxonomies }
139
+ t.string :name, null: false
140
+ t.string :slug, null: false
141
+ t.integer :position, default: 0
142
+ t.timestamps
143
+
144
+ t.index [ :taxonomy_id, :slug ], unique: true
145
+ end
146
+
147
+ create_table :plum_entry_terms do |t|
148
+ t.references :entry, null: false, foreign_key: { to_table: :plum_entries }
149
+ t.references :term, null: false, foreign_key: { to_table: :plum_terms }
150
+
151
+ t.index [ :entry_id, :term_id ], unique: true
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,54 @@
1
+ require "rails/generators"
2
+
3
+ module Plum
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ class_option :mount_path,
9
+ type: :string,
10
+ default: "/cms",
11
+ desc: "Path where Plum::Engine should be mounted"
12
+
13
+ class_option :skip_route,
14
+ type: :boolean,
15
+ default: false,
16
+ desc: "Skip adding the Plum engine mount to config/routes.rb"
17
+
18
+ def copy_initializer
19
+ template "plum_initializer.rb", "config/initializers/plum.rb"
20
+ end
21
+
22
+ def copy_migrations
23
+ rake "plum:install:migrations"
24
+ end
25
+
26
+ def create_theme_directory
27
+ empty_directory "app/themes"
28
+ end
29
+
30
+ def mount_engine
31
+ return if options[:skip_route]
32
+
33
+ route %(mount Plum::Engine, at: "#{options[:mount_path]}")
34
+ end
35
+
36
+ def print_next_steps
37
+ say <<~TEXT
38
+
39
+ Plum is installed.
40
+
41
+ Next steps:
42
+ bin/rails active_storage:install # if not already installed
43
+ bin/rails db:migrate
44
+
45
+ Plum is mounted at #{options[:mount_path]}.
46
+
47
+ Migrations are copied to db/migrate/ and can be reviewed
48
+ before running. JavaScript and CSS are served from the
49
+ engine automatically.
50
+ TEXT
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ Plum.configure do |config|
2
+ # Standalone/default mode: Plum manages its own site and users.
3
+ config.current_site_resolver = ->(_controller) { Plum::Site.first_or_create_standalone! }
4
+ config.current_user_resolver = lambda { |controller|
5
+ Plum::User.find_by(id: controller.session[:plum_user_id]) if controller.session[:plum_user_id]
6
+ }
7
+ config.authorize_with = :plum
8
+
9
+ # Host themes win first, bundled Plum themes provide the fallback.
10
+ config.theme_paths = [
11
+ Rails.root.join("app/themes"),
12
+ Plum::Engine.root.join("app/themes")
13
+ ]
14
+
15
+ # White-label the control panel:
16
+ #
17
+ # config.cp_name = "My Brand"
18
+ # config.cp_subtitle = "Website"
19
+ # config.cp_logo_path = "my-logo.svg"
20
+ # config.cp_accent_color = "#7c3aed"
21
+ # config.cp_sidebar_bg = "#241B27"
22
+ # config.cp_sidebar_header_bg = "#1E1621"
23
+ # config.cp_sidebar_text = "#D9CBD4"
24
+ # config.cp_sidebar_muted = "#A8929F"
25
+ # config.cp_back_url = "/dashboard"
26
+ # config.cp_back_label = "← Table Needs"
27
+
28
+ # Embedded host example:
29
+ #
30
+ # config.current_site_resolver = ->(_controller) { Current.restaurant.plum_site }
31
+ # config.current_user_resolver = ->(_controller) { Current.user }
32
+ # config.authorize_with = :host
33
+ # config.host_authorization_resolver = ->(_controller) { Current.user.can_manage_website?(Current.restaurant) }
34
+ # config.register_content_source :restaurant, ->(context) { context.site.owner.to_liquid }
35
+ # config.register_content_source :menu, "TableNeeds::PlumAdapters::Menu"
36
+ # config.register_content_source :hours, ->(context) { context.owner.hours_for_web }
37
+ end
@@ -0,0 +1,88 @@
1
+ require_relative "content_source_registry"
2
+
3
+ module Plum
4
+ class Configuration
5
+ attr_accessor :authorize_with, :current_site_resolver, :current_user_resolver, :host_authorization_resolver,
6
+ :mailer_sender, :cp_name, :cp_subtitle, :cp_logo_path,
7
+ :cp_accent_color, :cp_sidebar_bg, :cp_sidebar_header_bg, :cp_sidebar_text, :cp_sidebar_muted,
8
+ :cp_back_url, :cp_back_label,
9
+ :powered_by_name, :powered_by_url
10
+ attr_writer :theme_paths
11
+ attr_reader :content_sources
12
+
13
+ def initialize
14
+ @authorize_with = :plum
15
+ @mailer_sender = "no-reply@example.com"
16
+ @cp_name = "Plum"
17
+ @cp_subtitle = "CMS"
18
+ @cp_logo_path = "plum-mark.svg"
19
+ @cp_accent_color = "#7c3aed"
20
+ @cp_sidebar_bg = "#241B27"
21
+ @cp_sidebar_header_bg = "#1E1621"
22
+ @cp_sidebar_text = "#D9CBD4"
23
+ @cp_sidebar_muted = "#A8929F"
24
+ @cp_back_url = nil
25
+ @cp_back_label = "← Back"
26
+ @powered_by_name = "Plum"
27
+ @powered_by_url = "https://plumcms.com"
28
+ @content_sources = ContentSourceRegistry.new
29
+ @current_site_resolver = ->(_controller) { Plum::Site.first_or_create_standalone! }
30
+ @current_user_resolver = lambda { |controller|
31
+ Plum::User.find_by(id: controller.session[:plum_user_id]) if controller.session[:plum_user_id]
32
+ }
33
+ @host_authorization_resolver = ->(controller) { Plum.current_user(controller).present? }
34
+ end
35
+
36
+ def theme_paths
37
+ Array(@theme_paths.presence || default_theme_paths).map { |path| Pathname(path) }.uniq(&:to_s)
38
+ end
39
+
40
+ def register_content_source(handle, source = nil, &block)
41
+ content_sources.register(handle, source, &block)
42
+ end
43
+
44
+ private
45
+
46
+ def default_theme_paths
47
+ paths = []
48
+ paths << Rails.root.join("app/themes") if defined?(Rails) && Rails.root
49
+ paths << Plum::Engine.root.join("app/themes") if defined?(Plum::Engine)
50
+ paths
51
+ end
52
+ end
53
+
54
+ def self.configuration
55
+ @configuration ||= Configuration.new
56
+ end
57
+
58
+ def self.configure
59
+ yield configuration
60
+ end
61
+
62
+ def self.current_site(controller)
63
+ configuration.current_site_resolver.call(controller)
64
+ end
65
+
66
+ def self.current_user(controller)
67
+ configuration.current_user_resolver.call(controller)
68
+ end
69
+
70
+ def self.authorized?(controller)
71
+ case configuration.authorize_with
72
+ when :plum
73
+ current_user(controller).present?
74
+ when :host
75
+ configuration.host_authorization_resolver.call(controller)
76
+ else
77
+ configuration.authorize_with.respond_to?(:call) && configuration.authorize_with.call(controller)
78
+ end
79
+ end
80
+
81
+ def self.register_content_source(handle, source = nil, &block)
82
+ configuration.register_content_source(handle, source, &block)
83
+ end
84
+
85
+ def self.content_sources_for(controller, site:)
86
+ configuration.content_sources.to_liquid_context(controller: controller, site: site)
87
+ end
88
+ end
@@ -0,0 +1,45 @@
1
+ module Plum
2
+ class ContentSourceError < StandardError; end
3
+
4
+ class ContentSource
5
+ attr_reader :context
6
+
7
+ def initialize(context)
8
+ @context = context
9
+ end
10
+
11
+ def to_liquid
12
+ raise ContentSourceError, "#{self.class.name} must implement #to_liquid"
13
+ end
14
+
15
+ private
16
+
17
+ def controller
18
+ context.controller
19
+ end
20
+
21
+ def site
22
+ context.site
23
+ end
24
+
25
+ def owner
26
+ context.owner
27
+ end
28
+
29
+ def request
30
+ context.request
31
+ end
32
+
33
+ def params
34
+ context.params
35
+ end
36
+
37
+ def session
38
+ context.session
39
+ end
40
+
41
+ def current_user
42
+ context.current_user
43
+ end
44
+ end
45
+ end