alchemy_cms 8.3.11 → 8.4.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 (368) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +58 -10
  4. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  5. data/app/assets/builds/alchemy/admin/print.css +1 -1
  6. data/app/assets/builds/alchemy/admin/remote-select.css +1 -0
  7. data/app/assets/builds/alchemy/admin/tags-autocomplete.css +1 -0
  8. data/app/assets/builds/alchemy/admin.css +4 -4
  9. data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
  10. data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
  11. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  12. data/app/assets/builds/alchemy/light-theme.css +1 -1
  13. data/app/assets/builds/alchemy/preview.min.js +1 -1
  14. data/app/assets/builds/alchemy/theme.css +1 -1
  15. data/app/assets/builds/alchemy/welcome.css +1 -1
  16. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  17. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  18. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  19. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
  20. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  21. data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +1 -1
  22. data/app/components/alchemy/admin/dashboard/widgets/news_reader.html.erb +38 -0
  23. data/app/components/alchemy/admin/dashboard/widgets/news_reader.rb +54 -0
  24. data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +1 -1
  25. data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +1 -1
  26. data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +11 -3
  27. data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +6 -1
  28. data/app/components/alchemy/admin/element_editor.html.erb +1 -1
  29. data/app/components/alchemy/admin/element_select.rb +30 -20
  30. data/app/components/alchemy/admin/element_status_indicators.html.erb +16 -0
  31. data/app/components/alchemy/admin/element_status_indicators.rb +31 -0
  32. data/app/components/alchemy/admin/link_dialog/external_tab.rb +2 -1
  33. data/app/components/alchemy/admin/main_navigation/entry.rb +61 -0
  34. data/app/components/alchemy/admin/main_navigation/menu.rb +21 -0
  35. data/app/components/alchemy/admin/main_navigation/sub_navigation.rb +31 -0
  36. data/app/components/alchemy/admin/page_node.html.erb +1 -1
  37. data/app/components/alchemy/admin/page_permitted_roles_select.html.erb +11 -0
  38. data/app/components/alchemy/admin/page_permitted_roles_select.rb +35 -0
  39. data/app/components/alchemy/admin/page_select.rb +18 -8
  40. data/app/components/alchemy/admin/preview_time_select.rb +1 -1
  41. data/app/components/alchemy/admin/toolbar_button.rb +43 -20
  42. data/app/components/alchemy/admin/uploader_button.rb +66 -0
  43. data/app/components/alchemy/ingredients/base_editor.rb +10 -1
  44. data/app/components/alchemy/ingredients/boolean_editor.rb +1 -0
  45. data/app/components/alchemy/ingredients/color_editor.rb +6 -3
  46. data/app/components/alchemy/ingredients/datetime_view.rb +1 -1
  47. data/app/components/alchemy/ingredients/picture_view.rb +2 -4
  48. data/app/components/alchemy/menubar.html.erb +157 -0
  49. data/app/components/alchemy/menubar.rb +15 -0
  50. data/app/controllers/alchemy/admin/attachments_controller.rb +12 -9
  51. data/app/controllers/alchemy/admin/base_controller.rb +34 -21
  52. data/app/controllers/alchemy/admin/dashboard_controller.rb +4 -0
  53. data/app/controllers/alchemy/admin/elements_controller.rb +4 -23
  54. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  55. data/app/controllers/alchemy/admin/nodes_controller.rb +1 -1
  56. data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
  57. data/app/controllers/alchemy/admin/pictures_controller.rb +6 -3
  58. data/app/controllers/alchemy/admin/resources_controller.rb +5 -5
  59. data/app/controllers/alchemy/admin/sites_controller.rb +1 -1
  60. data/app/controllers/alchemy/base_controller.rb +24 -16
  61. data/app/controllers/alchemy/pages_controller.rb +29 -8
  62. data/app/controllers/alchemy/sitemap_controller.rb +58 -0
  63. data/app/controllers/concerns/alchemy/admin/csp_protection.rb +43 -0
  64. data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -1
  65. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +3 -1
  66. data/{lib → app/controllers/concerns}/alchemy/controller_actions.rb +13 -0
  67. data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +4 -4
  68. data/app/helpers/alchemy/admin/base_helper.rb +4 -1
  69. data/app/helpers/alchemy/admin/form_helper.rb +0 -2
  70. data/app/helpers/alchemy/admin/navigation_helper.rb +10 -181
  71. data/app/helpers/alchemy/elements_block_helper.rb +14 -0
  72. data/app/helpers/alchemy/resources_helper.rb +15 -1
  73. data/app/javascript/alchemy_admin/components/attachment_select.js +11 -20
  74. data/app/javascript/alchemy_admin/components/auto_submit.js +2 -4
  75. data/app/javascript/alchemy_admin/components/button.js +0 -6
  76. data/app/javascript/alchemy_admin/components/clear_schedule_button.js +21 -0
  77. data/app/javascript/alchemy_admin/components/color_select.js +50 -45
  78. data/app/javascript/alchemy_admin/components/conditional_field.js +50 -0
  79. data/app/javascript/alchemy_admin/components/dirty_guard.js +77 -0
  80. data/app/javascript/alchemy_admin/components/dom_id_select.js +17 -10
  81. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -2
  82. data/app/javascript/alchemy_admin/components/element_editor.js +29 -56
  83. data/app/javascript/alchemy_admin/components/element_select.js +26 -66
  84. data/app/javascript/alchemy_admin/components/icon.js +9 -4
  85. data/app/javascript/alchemy_admin/{image_cropper.js → components/image_cropper.js} +39 -53
  86. data/app/javascript/alchemy_admin/components/image_overlay_link.js +14 -0
  87. data/app/javascript/alchemy_admin/components/index.js +12 -1
  88. data/app/javascript/alchemy_admin/components/main_navi_entry.js +75 -0
  89. data/app/javascript/alchemy_admin/components/menu_node.js +73 -0
  90. data/app/javascript/alchemy_admin/components/news_reader.js +109 -0
  91. data/app/javascript/alchemy_admin/components/node_folder.js +36 -0
  92. data/app/javascript/alchemy_admin/components/node_form.js +34 -0
  93. data/app/javascript/alchemy_admin/components/node_select.js +25 -30
  94. data/app/javascript/alchemy_admin/components/node_tree.js +56 -0
  95. data/app/javascript/alchemy_admin/components/page_node.js +2 -1
  96. data/app/javascript/alchemy_admin/components/page_select.js +16 -34
  97. data/app/javascript/alchemy_admin/components/picture_archive.js +92 -0
  98. data/app/javascript/alchemy_admin/components/picture_editor.js +2 -1
  99. data/app/javascript/alchemy_admin/components/preview_window.js +11 -3
  100. data/app/javascript/alchemy_admin/components/remote_select.js +461 -81
  101. data/app/javascript/alchemy_admin/components/select.js +26 -88
  102. data/app/javascript/alchemy_admin/components/sitemap.js +2 -1
  103. data/app/javascript/alchemy_admin/components/sortable_elements.js +2 -1
  104. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +63 -48
  105. data/app/javascript/alchemy_admin/components/tinymce.js +2 -1
  106. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +3 -2
  107. data/app/javascript/alchemy_admin/components/uploader.js +19 -3
  108. data/app/javascript/alchemy_admin/confirm_dialog.js +7 -1
  109. data/app/javascript/alchemy_admin/dialog.js +182 -110
  110. data/app/javascript/alchemy_admin/hotkeys.js +0 -5
  111. data/app/javascript/alchemy_admin/i18n.js +3 -9
  112. data/app/javascript/alchemy_admin/image_overlay.js +58 -42
  113. data/app/javascript/alchemy_admin/initializer.js +2 -2
  114. data/app/javascript/alchemy_admin/link_dialog.js +15 -2
  115. data/app/javascript/alchemy_admin/routes.js +18 -0
  116. data/app/javascript/alchemy_admin/turbo_stream_actions.js +88 -0
  117. data/app/javascript/alchemy_admin/utils/ajax.js +14 -3
  118. data/app/javascript/alchemy_admin/utils/events.js +5 -0
  119. data/app/javascript/alchemy_admin/utils/json_script.js +12 -0
  120. data/app/javascript/alchemy_admin/utils/tom_select.js +171 -0
  121. data/app/javascript/alchemy_admin.js +2 -19
  122. data/app/javascript/preview.js +131 -113
  123. data/app/jobs/alchemy/invalidate_elements_cache_job.rb +1 -1
  124. data/app/models/alchemy/admin/content_security_policy.rb +143 -0
  125. data/app/models/alchemy/admin/filters/base.rb +7 -0
  126. data/app/models/alchemy/admin/filters/datepicker.rb +1 -1
  127. data/app/models/alchemy/admin/filters/select.rb +20 -7
  128. data/app/models/alchemy/attachment.rb +14 -7
  129. data/app/models/alchemy/cache_control.rb +104 -0
  130. data/app/models/alchemy/current.rb +1 -1
  131. data/app/models/alchemy/eager_loading.rb +0 -2
  132. data/app/models/alchemy/element.rb +23 -8
  133. data/app/models/alchemy/element_definition.rb +1 -0
  134. data/app/models/alchemy/elements_repository.rb +11 -0
  135. data/app/models/alchemy/folded_page.rb +5 -1
  136. data/app/models/alchemy/ingredient.rb +26 -0
  137. data/app/models/alchemy/ingredients/datetime.rb +1 -1
  138. data/app/models/alchemy/ingredients/link.rb +2 -0
  139. data/app/models/alchemy/ingredients/picture.rb +3 -0
  140. data/app/models/alchemy/ingredients/text.rb +2 -0
  141. data/app/models/alchemy/language.rb +19 -7
  142. data/app/models/alchemy/legacy_page_url.rb +6 -1
  143. data/app/models/alchemy/node.rb +51 -0
  144. data/app/models/alchemy/page/page_natures.rb +41 -8
  145. data/app/models/alchemy/page/page_scopes.rb +23 -0
  146. data/app/models/alchemy/page/sitemap_cache_key.rb +56 -0
  147. data/app/models/alchemy/page.rb +88 -32
  148. data/app/models/alchemy/page_definition.rb +1 -1
  149. data/app/models/alchemy/page_mutex.rb +16 -0
  150. data/app/models/alchemy/page_version.rb +24 -1
  151. data/app/models/alchemy/permissions.rb +6 -5
  152. data/app/models/alchemy/picture.rb +15 -7
  153. data/app/models/alchemy/picture_description.rb +22 -0
  154. data/app/models/alchemy/picture_thumb.rb +18 -0
  155. data/app/models/alchemy/publishable/timestamp_resolver.rb +70 -0
  156. data/app/models/alchemy/resource.rb +1 -1
  157. data/app/models/alchemy/safe_url_validator.rb +34 -0
  158. data/app/models/alchemy/site.rb +8 -3
  159. data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +7 -3
  160. data/app/models/alchemy/storage_adapter/active_storage.rb +4 -1
  161. data/app/models/alchemy/tag.rb +10 -4
  162. data/{lib → app/models/concerns}/alchemy/filetypes.rb +5 -0
  163. data/app/models/concerns/alchemy/picture_thumbnails.rb +61 -6
  164. data/app/models/concerns/alchemy/publishable.rb +14 -27
  165. data/app/models/concerns/alchemy/taggable.rb +92 -0
  166. data/app/models/concerns/alchemy/user_methods.rb +95 -0
  167. data/app/services/alchemy/duplicate_element.rb +5 -2
  168. data/app/services/alchemy/element_preloader.rb +1 -2
  169. data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
  170. data/app/stylesheets/alchemy/_extends.scss +0 -8
  171. data/app/stylesheets/alchemy/_themes.scss +10 -9
  172. data/app/stylesheets/alchemy/admin/_empty-state.scss +29 -0
  173. data/app/stylesheets/alchemy/admin/_help.scss +10 -0
  174. data/app/stylesheets/alchemy/admin/_tom-select.scss +104 -15
  175. data/app/stylesheets/alchemy/admin/archive.scss +56 -11
  176. data/app/stylesheets/alchemy/admin/base.scss +4 -5
  177. data/app/stylesheets/alchemy/admin/buttons.scss +12 -1
  178. data/app/stylesheets/alchemy/admin/dashboard.scss +150 -20
  179. data/app/stylesheets/alchemy/admin/dialogs.scss +27 -36
  180. data/app/stylesheets/alchemy/admin/element-select.scss +1 -1
  181. data/app/stylesheets/alchemy/admin/elements.scss +8 -24
  182. data/app/stylesheets/alchemy/admin/flash.scss +2 -1
  183. data/app/stylesheets/alchemy/admin/forms.scss +60 -24
  184. data/app/stylesheets/alchemy/admin/frame.scss +40 -4
  185. data/app/stylesheets/alchemy/admin/image_library.scss +9 -12
  186. data/app/stylesheets/alchemy/admin/navigation.scss +17 -14
  187. data/app/stylesheets/alchemy/admin/node-select.scss +13 -37
  188. data/app/stylesheets/alchemy/admin/nodes.scss +1 -1
  189. data/app/stylesheets/alchemy/admin/page-select.scss +6 -97
  190. data/app/stylesheets/alchemy/admin/print.scss +0 -4
  191. data/app/stylesheets/alchemy/admin/remote-select.scss +170 -0
  192. data/app/stylesheets/alchemy/admin/search.scss +2 -3
  193. data/app/stylesheets/alchemy/admin/selects.scss +1 -294
  194. data/app/stylesheets/alchemy/admin/sitemap.scss +2 -1
  195. data/app/stylesheets/alchemy/admin/tables.scss +7 -1
  196. data/app/stylesheets/alchemy/admin/tags-autocomplete.scss +17 -0
  197. data/app/stylesheets/alchemy/admin/tags.scss +0 -13
  198. data/app/stylesheets/alchemy/admin/toolbar.scss +9 -6
  199. data/app/stylesheets/alchemy/admin/upload.scss +61 -1
  200. data/app/stylesheets/alchemy/admin.scss +4 -1
  201. data/app/types/alchemy/cache_control_type.rb +84 -0
  202. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  203. data/app/views/alchemy/_menubar.html.erb +2 -154
  204. data/app/views/alchemy/_preview_mode_code.html.erb +0 -3
  205. data/app/views/alchemy/admin/_left_menu.html.erb +18 -5
  206. data/app/views/alchemy/admin/_top_menu.html.erb +1 -1
  207. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +5 -4
  208. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
  209. data/app/views/alchemy/admin/attachments/_files_list.html.erb +71 -83
  210. data/app/views/alchemy/admin/attachments/assign.turbo_stream.erb +8 -0
  211. data/app/views/alchemy/admin/attachments/index.html.erb +32 -7
  212. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  213. data/app/views/alchemy/admin/crop.html.erb +20 -20
  214. data/app/views/alchemy/admin/dashboard/_footer.html.erb +0 -3
  215. data/app/views/alchemy/admin/dashboard/_top.html.erb +1 -0
  216. data/app/views/alchemy/admin/dashboard/help.html.erb +84 -57
  217. data/app/views/alchemy/admin/dashboard/license.html.erb +6 -0
  218. data/app/views/alchemy/admin/elements/_footer.html.erb +0 -1
  219. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  220. data/app/views/alchemy/admin/elements/_header.html.erb +2 -19
  221. data/app/views/alchemy/admin/elements/_preview_text_quote.html.erb +3 -0
  222. data/app/views/alchemy/admin/elements/_schedule.html.erb +7 -7
  223. data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -1
  224. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +1 -1
  225. data/app/views/alchemy/admin/elements/index.html.erb +1 -1
  226. data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +2 -2
  227. data/app/views/alchemy/admin/elements/update.turbo_stream.erb +31 -0
  228. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +6 -3
  229. data/app/views/alchemy/admin/languages/_table.html.erb +0 -2
  230. data/app/views/alchemy/admin/languages/index.html.erb +25 -29
  231. data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +1 -3
  232. data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
  233. data/app/views/alchemy/admin/layoutpages/index.html.erb +26 -7
  234. data/app/views/alchemy/admin/nodes/_form.html.erb +3 -21
  235. data/app/views/alchemy/admin/nodes/_node.html.erb +93 -88
  236. data/app/views/alchemy/admin/nodes/index.html.erb +15 -23
  237. data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -6
  238. data/app/views/alchemy/admin/pages/_form.html.erb +2 -1
  239. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +1 -1
  240. data/app/views/alchemy/admin/pages/_table.html.erb +2 -4
  241. data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -1
  242. data/app/views/alchemy/admin/pages/edit.html.erb +10 -21
  243. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +1 -1
  244. data/app/views/alchemy/admin/pages/index.html.erb +26 -31
  245. data/app/views/alchemy/admin/pages/info.html.erb +2 -2
  246. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +2 -2
  247. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
  248. data/app/views/alchemy/admin/partials/_routes.html.erb +24 -48
  249. data/app/views/alchemy/admin/pictures/_archive.html.erb +3 -17
  250. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  251. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -3
  252. data/app/views/alchemy/admin/pictures/_form.html.erb +0 -1
  253. data/app/views/alchemy/admin/pictures/_infos.html.erb +5 -1
  254. data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
  255. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -4
  256. data/app/views/alchemy/admin/pictures/assign.turbo_stream.erb +7 -0
  257. data/app/views/alchemy/admin/pictures/index.html.erb +38 -30
  258. data/app/views/alchemy/admin/pictures/show.html.erb +41 -41
  259. data/app/views/alchemy/admin/resources/_empty_state.html.erb +27 -0
  260. data/app/views/alchemy/admin/resources/_pagination.html.erb +1 -1
  261. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
  262. data/app/views/alchemy/admin/resources/_resource_table.html.erb +0 -2
  263. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +3 -2
  264. data/app/views/alchemy/admin/resources/index.html.erb +32 -13
  265. data/app/views/alchemy/admin/sites/index.html.erb +24 -22
  266. data/app/views/alchemy/admin/styleguide/index.html.erb +35 -12
  267. data/app/views/alchemy/admin/tags/index.html.erb +27 -25
  268. data/app/views/alchemy/admin/tinymce/_setup.html.erb +6 -8
  269. data/app/views/alchemy/admin/translations/_en.js +3 -0
  270. data/app/views/alchemy/admin/uploader/_button.html.erb +11 -17
  271. data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -3
  272. data/app/views/alchemy/pages/sitemap.xml.erb +2 -0
  273. data/app/views/kaminari/alchemy/_first_page.html.erb +1 -2
  274. data/app/views/kaminari/alchemy/_gap.html.erb +0 -1
  275. data/app/views/kaminari/alchemy/_last_page.html.erb +1 -2
  276. data/app/views/kaminari/alchemy/_next_page.html.erb +1 -2
  277. data/app/views/kaminari/alchemy/_page.html.erb +1 -2
  278. data/app/views/kaminari/alchemy/_paginator.html.erb +0 -1
  279. data/app/views/kaminari/alchemy/_prev_page.html.erb +1 -2
  280. data/app/views/layouts/alchemy/admin/dialog.html.erb +3 -0
  281. data/app/views/layouts/alchemy/admin.html.erb +5 -6
  282. data/config/brakeman.ignore +23 -1
  283. data/config/importmap.rb +10 -8
  284. data/config/initializers/dragonfly.rb +21 -17
  285. data/config/locales/alchemy.en.yml +52 -11
  286. data/config/routes.rb +3 -3
  287. data/db/migrate/20260727120000_add_permitted_roles_to_alchemy_pages.rb +23 -0
  288. data/db/migrate/20260820120000_add_cached_tag_list_to_taggables.rb +57 -0
  289. data/lib/alchemy/configuration/number_option.rb +13 -0
  290. data/lib/alchemy/configuration/proc_option.rb +13 -0
  291. data/lib/alchemy/configuration.rb +2 -0
  292. data/lib/alchemy/configurations/admin_components.rb +30 -0
  293. data/lib/alchemy/configurations/format_matchers.rb +7 -1
  294. data/lib/alchemy/configurations/main.rb +103 -4
  295. data/lib/alchemy/configurations/menu.rb +91 -0
  296. data/lib/alchemy/configurations/menu_item.rb +123 -0
  297. data/lib/alchemy/configurations/sitemap.rb +1 -0
  298. data/lib/alchemy/configurations/uploader.rb +15 -1
  299. data/lib/alchemy/engine.rb +0 -6
  300. data/lib/alchemy/i18n.rb +26 -0
  301. data/lib/alchemy/modules.rb +52 -50
  302. data/lib/alchemy/tasks/generate_thumbnails.rb +99 -0
  303. data/lib/alchemy/tasks/tidy.rb +1 -1
  304. data/lib/alchemy/test_support/capybara_helpers.rb +48 -37
  305. data/lib/alchemy/test_support/current_language_shared_examples.rb +1 -1
  306. data/lib/alchemy/test_support/factories/dummy_user_factory.rb +4 -1
  307. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +132 -0
  308. data/lib/alchemy/test_support/shared_ingredient_examples.rb +52 -0
  309. data/lib/alchemy/test_support/shared_publishable_examples.rb +29 -4
  310. data/lib/alchemy/upgrader/eight_four.rb +237 -0
  311. data/lib/alchemy/upgrader/register_module_rewriter.rb +177 -0
  312. data/lib/alchemy/upgrader.rb +2 -1
  313. data/lib/alchemy/version.rb +1 -1
  314. data/lib/alchemy.rb +0 -1
  315. data/lib/alchemy_cms.rb +0 -13
  316. data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
  317. data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
  318. data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
  319. data/lib/generators/alchemy/install/install_generator.rb +2 -13
  320. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +24 -9
  321. data/lib/generators/alchemy/module/module_generator.rb +1 -0
  322. data/lib/generators/alchemy/module/templates/ability.rb.tt +1 -1
  323. data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -18
  324. data/lib/generators/alchemy/user_columns_migration/templates/migration.rb.tt +19 -0
  325. data/lib/generators/alchemy/user_columns_migration/user_columns_migration_generator.rb +30 -0
  326. data/lib/tasks/alchemy/thumbnails.rake +16 -29
  327. data/lib/tasks/alchemy/upgrade.rake +45 -3
  328. data/vendor/javascript/cropperjs.min.js +3 -3
  329. data/vendor/javascript/floating-ui.min.js +1 -1
  330. data/vendor/javascript/tinymce.min.js +5 -5
  331. data/vendor/javascript/tom-select.min.js +1 -1
  332. metadata +85 -65
  333. data/app/javascript/alchemy_admin/components/action.js +0 -50
  334. data/app/javascript/alchemy_admin/dirty.js +0 -55
  335. data/app/javascript/alchemy_admin/node_tree.js +0 -82
  336. data/app/javascript/alchemy_admin/picture_selector.js +0 -67
  337. data/app/javascript/alchemy_admin/templates/compiled.js +0 -1
  338. data/app/javascript/alchemy_admin/templates/node_folder.hbs +0 -3
  339. data/app/stylesheets/alchemy/admin/attachment-select.scss +0 -19
  340. data/app/views/alchemy/admin/_main_navi.html.erb +0 -6
  341. data/app/views/alchemy/admin/attachments/assign.js.erb +0 -15
  342. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +0 -30
  343. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +0 -20
  344. data/app/views/alchemy/admin/partials/_sub_navigation.html.erb +0 -11
  345. data/app/views/alchemy/admin/pictures/assign.js.erb +0 -13
  346. data/app/views/alchemy/base/error_notice.js.erb +0 -1
  347. data/app/views/alchemy/base/permission_denied.js.erb +0 -3
  348. data/app/views/alchemy/base/redirect.js.erb +0 -11
  349. data/config/alchemy/config.yml +0 -217
  350. data/config/alchemy/modules.yml +0 -82
  351. data/config/initializers/rails_live_reload.rb +0 -13
  352. data/lib/alchemy/dev_support/live_reload_watcher.rb +0 -5
  353. data/lib/alchemy/taggable.rb +0 -49
  354. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -34
  355. data/vendor/assets/stylesheets/alchemy_admin/select2.css +0 -1
  356. data/vendor/javascript/handlebars.min.js +0 -29
  357. data/vendor/javascript/jquery.min.js +0 -2
  358. data/vendor/javascript/rails-ujs.min.js +0 -1
  359. data/vendor/javascript/select2.min.js +0 -23
  360. /data/{lib → app/controllers/concerns}/alchemy/ability_helper.rb +0 -0
  361. /data/{lib → app/controllers/concerns}/alchemy/admin/locale.rb +0 -0
  362. /data/{lib → app/controllers/concerns}/alchemy/admin/preview_time.rb +0 -0
  363. /data/{lib → app/controllers/concerns}/alchemy/admin/timezone.rb +0 -0
  364. /data/{lib → app/controllers/concerns}/alchemy/configuration_methods.rb +0 -0
  365. /data/{lib → app/models}/alchemy/admin/preview_url.rb +0 -0
  366. /data/{lib → app/models/concerns}/alchemy/hints.rb +0 -0
  367. /data/{lib → app/models/concerns}/alchemy/name_conversions.rb +0 -0
  368. /data/{lib → app/services}/alchemy/elements_finder.rb +0 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # Validates that a url does not carry an executable scheme.
5
+ #
6
+ # Only schemes listed in +Alchemy.config.allowed_url_schemes+ pass. Urls
7
+ # without a scheme (absolute and relative paths, anchors, queries and
8
+ # protocol relative urls) pass as well, they cannot carry executable code.
9
+ #
10
+ # validates_with Alchemy::SafeUrlValidator, attributes: [:url]
11
+ #
12
+ class SafeUrlValidator < ActiveModel::EachValidator
13
+ SCHEME_REGEX = /\A(?<scheme>[a-z][a-z0-9+.-]*):/i
14
+
15
+ # Browsers drop these before they parse a url, which makes "java\tscript:"
16
+ # a working "javascript:" url. They have to go before the scheme is read.
17
+ STRIPPED_CHARS = /[\u0000-\u0020]/
18
+
19
+ def validate_each(record, attribute, value)
20
+ return if value.blank?
21
+
22
+ scheme = SCHEME_REGEX.match(value.to_s.gsub(STRIPPED_CHARS, ""))&.[](:scheme)&.downcase
23
+ return if scheme.nil? || allowed_schemes.include?(scheme)
24
+
25
+ record.errors.add(attribute, :invalid_url_scheme, scheme: scheme)
26
+ end
27
+
28
+ private
29
+
30
+ def allowed_schemes
31
+ Alchemy.config.allowed_url_schemes.map(&:downcase)
32
+ end
33
+ end
34
+ end
@@ -5,13 +5,18 @@
5
5
  # Table name: alchemy_sites
6
6
  #
7
7
  # id :integer not null, primary key
8
+ # aliases :text
8
9
  # host :string
9
10
  # name :string
11
+ # public :boolean default(FALSE), not null
12
+ # redirect_to_primary_host :boolean default(FALSE), not null
10
13
  # created_at :datetime not null
11
14
  # updated_at :datetime not null
12
- # public :boolean default(FALSE)
13
- # aliases :text
14
- # redirect_to_primary_host :boolean
15
+ #
16
+ # Indexes
17
+ #
18
+ # alchemy_sites_public_hosts_idx (host,public)
19
+ # index_alchemy_sites_on_host (host)
15
20
  #
16
21
 
17
22
  module Alchemy
@@ -23,9 +23,13 @@ module Alchemy
23
23
  attr_reader :attachable
24
24
 
25
25
  class << self
26
- def generate_thumbs!(attachable)
27
- Alchemy::Picture::THUMBNAIL_SIZES.values.each do |size|
28
- process_thumb(attachable, size: size, flatten: true)
26
+ def generate_thumbs!(picture)
27
+ format = picture.image_file_extension || "jpg"
28
+ Alchemy::Picture::THUMBNAIL_SIZES.each_value do |size|
29
+ transformations = Alchemy::DragonflyToImageProcessing.call(
30
+ flatten: true, format: format, size: size
31
+ )
32
+ ::ActiveStorage::TransformJob.perform_later(picture.image_file.blob, transformations)
29
33
  end
30
34
  end
31
35
 
@@ -5,13 +5,16 @@ module Alchemy
5
5
  def self.included(base)
6
6
  base.has_one_attached :image_file do |attachable|
7
7
  Alchemy.storage_adapter.preprocessor_class.new(attachable).call
8
- Alchemy.storage_adapter.preprocessor_class.generate_thumbs!(attachable)
9
8
  end
10
9
 
11
10
  base.after_create_commit if: :svg? do
12
11
  SanitizeSvgJob.perform_later(self, file_accessor: :image_file)
13
12
  end
14
13
 
14
+ base.after_create_commit if: :has_convertible_format? do
15
+ Alchemy.storage_adapter.preprocessor_class.generate_thumbs!(self)
16
+ end
17
+
15
18
  # The image file's dirty state is cleared by the time
16
19
  # +after_update_commit+ runs, so capture it here to know whether the
17
20
  # blob was actually replaced (rather than only its metadata updated).
@@ -5,8 +5,15 @@
5
5
  # Table name: gutentag_tags
6
6
  #
7
7
  # id :integer not null, primary key
8
- # name :string
9
- # taggings_count :integer default(0)
8
+ # name :string not null
9
+ # taggings_count :integer default(0), not null
10
+ # created_at :datetime not null
11
+ # updated_at :datetime not null
12
+ #
13
+ # Indexes
14
+ #
15
+ # index_gutentag_tags_on_name (name) UNIQUE
16
+ # index_gutentag_tags_on_taggings_count (taggings_count)
10
17
  #
11
18
 
12
19
  # Just holds some useful tag methods.
@@ -24,8 +31,7 @@ module Alchemy
24
31
  # Replaces tag with new tag on all models tagged with tag.
25
32
  def self.replace(tag, new_tag)
26
33
  tag.taggings.collect(&:taggable).each do |taggable|
27
- taggable.tag_list.delete(tag.name)
28
- taggable.tag_list << new_tag.name
34
+ taggable.tag_names = taggable.tag_names - [tag.name] + [new_tag.name]
29
35
  taggable.save
30
36
  end
31
37
  end
@@ -5,9 +5,13 @@ module Alchemy
5
5
  ARCHIVE_FILE_TYPES = ["application/zip", "application/x-rar"]
6
6
 
7
7
  AUDIO_FILE_TYPES = [
8
+ "audio/aac",
9
+ "audio/flac",
8
10
  "audio/mpeg",
9
11
  "audio/mp4",
12
+ "audio/ogg",
10
13
  "audio/wav",
14
+ "audio/webm",
11
15
  "audio/x-wav"
12
16
  ]
13
17
 
@@ -29,6 +33,7 @@ module Alchemy
29
33
  "video/x-flv",
30
34
  "video/mp4",
31
35
  "video/mpeg",
36
+ "video/ogg",
32
37
  "video/quicktime",
33
38
  "video/webm",
34
39
  "video/x-msvideo",
@@ -37,27 +37,74 @@ module Alchemy
37
37
  def picture_url(options = {})
38
38
  return if picture.nil?
39
39
 
40
- picture.url(picture_url_options.merge(options)) || "missing-image.png"
40
+ picture.url(picture_url_options(options)) || "missing-image.png"
41
41
  end
42
42
 
43
43
  # Picture rendering options
44
44
  #
45
- # Returns the +default_render_format+ of the associated +Alchemy::Picture+
46
- # together with the +crop_from+ and +crop_size+ values
45
+ # Returns the render format of the associated +Alchemy::Picture+
46
+ # together with the +crop_from+ and +crop_size+ values.
47
+ #
48
+ # The render format is taken from the +format+ option, the +format+ setting
49
+ # of the ingredient definition or the +default_render_format+ of the picture.
50
+ # A format is only applied if the picture can be converted.
51
+ #
52
+ # @param options [Hash] Options that override the defaults (i.e. +size+ or +format+ of a srcset entry)
47
53
  #
48
54
  # @return [HashWithIndifferentAccess]
49
- def picture_url_options
55
+ def picture_url_options(options = {})
50
56
  return {} if picture.nil?
51
57
 
58
+ options = options.symbolize_keys
52
59
  crop = !!settings[:crop]
53
60
 
54
61
  {
55
- format: picture.default_render_format,
62
+ format: picture_render_format(options.delete(:format) || settings[:format]),
56
63
  crop: crop,
57
64
  crop_from: crop && crop_from.presence || nil,
58
65
  crop_size: crop && crop_size.presence || nil,
59
66
  size: settings[:size]
60
- }.with_indifferent_access
67
+ }.merge(options).with_indifferent_access
68
+ end
69
+
70
+ # Picture rendering options for each entry of the +srcset+ setting
71
+ #
72
+ # Entries can either be a size string (i.e. +"800x600"+) or a hash
73
+ # with +size+ and optional +format+ keys.
74
+ #
75
+ # @param srcset [Array<String, Hash>] Defaults to the +srcset+ setting
76
+ #
77
+ # @return [Array<HashWithIndifferentAccess>]
78
+ def picture_srcset_variant_options(srcset = settings.fetch(:srcset, []))
79
+ return [] if picture.nil?
80
+
81
+ srcset.map do |src|
82
+ picture_url_options(src.is_a?(Hash) ? src : {size: src})
83
+ end
84
+ end
85
+
86
+ # Url, dimensions and mime type for each entry of the +srcset+ setting
87
+ #
88
+ # === Example:
89
+ #
90
+ # picture_srcset_options
91
+ # # => [{url: "/pictures/1/360x360/cats.webp", desc: "360w", width: "360", height: "360", type: "image/webp"}]
92
+ #
93
+ # @param srcset [Array<String, Hash>] Defaults to the +srcset+ setting
94
+ #
95
+ # @return [Array<Hash>]
96
+ def picture_srcset_options(srcset = settings.fetch(:srcset, []))
97
+ picture_srcset_variant_options(srcset).map do |options|
98
+ width, height = options[:size].to_s.split("x", 2)
99
+
100
+ {
101
+ url: picture_url(options),
102
+ desc: width.present? ? "#{width}w" : "#{height}h",
103
+ width: width,
104
+ height: height,
105
+ type: Mime::Type.lookup_by_extension(options[:format]).to_s
106
+ }
107
+ end
61
108
  end
62
109
 
63
110
  # Returns an url for the thumbnail representation of the assigned picture
@@ -109,6 +156,14 @@ module Alchemy
109
156
 
110
157
  private
111
158
 
159
+ def picture_render_format(format)
160
+ if format.present? && picture.has_convertible_format?
161
+ format.to_s
162
+ else
163
+ picture.default_render_format
164
+ end
165
+ end
166
+
112
167
  # An Image smaller than dimensions
113
168
  # can not be cropped to given size - unless upsample is true.
114
169
  #
@@ -3,13 +3,10 @@ module Alchemy
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- scope :draft, -> { where(public_on: nil) }
7
- scope :scheduled, -> { where.not(public_on: nil) }
8
-
6
+ scope :draft, -> { Alchemy.config.publishable_resolver.draft(all) }
7
+ scope :scheduled, -> { Alchemy.config.publishable_resolver.scheduled(all) }
9
8
  scope :published, ->(at: Current.preview_time) {
10
- scheduled
11
- .where("#{table_name}.public_on <= :at", at:)
12
- .where("#{table_name}.public_until IS NULL OR #{table_name}.public_until > :at", at:)
9
+ Alchemy.config.publishable_resolver.published(all, at:)
13
10
  }
14
11
 
15
12
  validate do
@@ -23,42 +20,32 @@ module Alchemy
23
20
 
24
21
  # Determines if this record is public
25
22
  #
26
- # Takes the two timestamps +public_on+ and +public_until+
27
- # and returns true if the time given (+Time.current+ per default)
28
- # is in this timespan.
29
- #
30
- # @param at [DateTime] (Time.current)
23
+ # @param at [DateTime] (Current.preview_time)
31
24
  # @returns Boolean
32
25
  def public?(at: Current.preview_time)
33
- already_public_for?(at:) && still_public_for?(at:)
26
+ publishable_resolver.public?(at:)
34
27
  end
35
28
  alias_method :public, :public?
36
29
 
30
+ # Determines if this record has a future publication or expiration event
31
+ #
32
+ # @param at [DateTime] (Current.preview_time)
33
+ # @returns Boolean
37
34
  def scheduled?(at: Current.preview_time)
38
- (public_on.present? && public_on > at) || (public_until.present? && public_until > at)
35
+ publishable_resolver.scheduled?(at:)
39
36
  end
40
37
 
41
38
  # Determines if this record is publishable
42
39
  #
43
- # A record is publishable if a +public_on+ timestamp is set and not expired yet.
44
- #
45
40
  # @returns Boolean
46
41
  def publishable?
47
- !public_on.nil? && still_public_for?(at: Time.current)
42
+ publishable_resolver.publishable?
48
43
  end
49
44
 
50
- # Determines if this record is already public for given time
51
- # @param at [DateTime] (Time.current)
52
- # @returns Boolean
53
- def already_public_for?(at: Current.preview_time)
54
- !public_on.nil? && public_on <= at
55
- end
45
+ private
56
46
 
57
- # Determines if this record is still public for given time
58
- # @param at [DateTime] (Time.current)
59
- # @returns Boolean
60
- def still_public_for?(at: Current.preview_time)
61
- public_until.nil? || public_until > at
47
+ def publishable_resolver
48
+ @_publishable_resolver ||= Alchemy.config.publishable_resolver.new(self)
62
49
  end
63
50
  end
64
51
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # ActsAsTaggableOn to Gutentag interface compatibility module
5
+ # Include this module to add tagging support to your model.
6
+ module Taggable
7
+ def self.included(base)
8
+ Gutentag::ActiveRecord.call base
9
+ base.extend ClassMethods
10
+
11
+ # Opt-in denormalized cache, maintained only for taggables whose table
12
+ # has a cached_tag_list column. serialize defers until the schema loads,
13
+ # so declaring it unconditionally never touches the DB at boot and is
14
+ # harmless for taggables without the column.
15
+ base.attribute :cached_tag_list, default: -> { [] }
16
+ base.serialize :cached_tag_list, coder: JSON
17
+ base.before_save :remember_tag_name_change
18
+ base.after_save :cache_tag_list
19
+ end
20
+
21
+ # The public list of tag names. Reads the denormalized cache column so the
22
+ # common read paths (rendering, serializing) issue no query. Models without
23
+ # the cache column fall back to gutentag's lazy-loading tag_names.
24
+ #
25
+ # The column is nullable, so rows written before the cache was introduced
26
+ # read back nil, which we coerce to an empty array.
27
+ def tag_list
28
+ caches_tag_list? ? (cached_tag_list || []) : tag_names
29
+ end
30
+
31
+ # Set a list of tags
32
+ # Pass a String with comma separated tag names or
33
+ # an Array of tag names
34
+ def tag_list=(tags)
35
+ case tags
36
+ when String
37
+ self.tag_names = tags.split(/,\s*/)
38
+ when Array
39
+ self.tag_names = tags
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # gutentag reconciles the tags in an after_save callback, and reading its
46
+ # tag_names accessor before that runs would lazy-load from the still-empty
47
+ # tags association of an unsaved record. So we remember whether the tags
48
+ # changed here (query-free) and read the reconciled value after the save.
49
+ def remember_tag_name_change
50
+ @tag_names_changed = will_save_change_to_tag_names?
51
+ true
52
+ end
53
+
54
+ # Keeps the denormalized cached_tag_list column in sync with the tags.
55
+ # Only runs when the tags actually changed, so untagged saves stay
56
+ # query-free.
57
+ def cache_tag_list
58
+ return unless @tag_names_changed
59
+ return unless caches_tag_list?
60
+
61
+ update_column(:cached_tag_list, Array(tag_names))
62
+ end
63
+
64
+ def caches_tag_list?
65
+ self.class.column_names.include?("cached_tag_list")
66
+ end
67
+
68
+ module ClassMethods
69
+ def tagged_with(names = [], **args)
70
+ if names.is_a? String
71
+ names = names.split(/,\s*/)
72
+ end
73
+
74
+ unless args[:match]
75
+ args[:match] = :all
76
+ end
77
+
78
+ if names.any?
79
+ args[:names] = names
80
+ end
81
+
82
+ super(args)
83
+ end
84
+
85
+ # Returns all unique tags
86
+ def tag_counts
87
+ Gutentag::Tag.distinct.joins(:taggings)
88
+ .where(gutentag_taggings: {taggable_type: name})
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,95 @@
1
+ module Alchemy
2
+ module UserMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Unlock all locked pages before destroy.
7
+ before_destroy :unlock_pages!
8
+
9
+ scope :alchemy_admins, -> { with_alchemy_role(:admin) }
10
+
11
+ scope :with_alchemy_role, ->(role) {
12
+ role = role.to_s.downcase
13
+ unless Alchemy.config.user_roles.include?(role)
14
+ Alchemy::Logger.warn("Unknown Alchemy role: #{role.inspect}")
15
+ end
16
+
17
+ col = arel_table[:alchemy_roles]
18
+ where(
19
+ col.eq(role)
20
+ .or(col.matches("#{role} %"))
21
+ .or(col.matches("% #{role} %"))
22
+ .or(col.matches("% #{role}"))
23
+ )
24
+ }
25
+
26
+ validates :alchemy_roles,
27
+ inclusion: {
28
+ in: Alchemy.config.user_roles,
29
+ message: "is not included in #{Alchemy.config.user_roles.join(", ")}"
30
+ }
31
+
32
+ # Returns all pages locked by user.
33
+ #
34
+ # A page gets locked when the user starts to edit the page.
35
+ #
36
+ has_many :locked_pages,
37
+ -> { order(:locked_at) },
38
+ foreign_key: :locked_by,
39
+ class_name: "Alchemy::Page",
40
+ inverse_of: :locker
41
+ end
42
+
43
+ class_methods do
44
+ def human_alchemy_rolename(role)
45
+ Alchemy.t("user_roles.#{role}")
46
+ end
47
+ end
48
+
49
+ def alchemy_admin?
50
+ has_alchemy_role?("admin")
51
+ end
52
+
53
+ def alchemy_roles
54
+ if self.class.columns_hash["alchemy_roles"]
55
+ read_attribute(:alchemy_roles).to_s.split(" ")
56
+ else
57
+ Alchemy::Logger.warn <<-WARN.strip_heredoc
58
+ The `alchemy_roles` column is missing in the #{self.class.table_name} table!
59
+
60
+ Please run
61
+
62
+ rails generate alchemy:user_columns_migration
63
+
64
+ to add it.
65
+
66
+ Falling back to default role: `member`.
67
+ WARN
68
+ ["member"]
69
+ end
70
+ end
71
+
72
+ def alchemy_roles=(roles)
73
+ roles = Array(roles).map { _1.to_s.strip }
74
+ roles.reject!(&:blank?)
75
+ roles.uniq!
76
+ super(roles.join(" "))
77
+ end
78
+
79
+ # Returns true if the user has the given role.
80
+ def has_alchemy_role?(role)
81
+ alchemy_roles.include?(role.to_s)
82
+ end
83
+
84
+ def human_alchemy_roles
85
+ alchemy_roles.map do |role|
86
+ self.class.human_alchemy_rolename(role)
87
+ end.to_sentence
88
+ end
89
+
90
+ # Calls unlock on all locked pages
91
+ def unlock_pages!
92
+ locked_pages.each(&:unlock!)
93
+ end
94
+ end
95
+ end
@@ -4,6 +4,7 @@ module Alchemy
4
4
  class DuplicateElement
5
5
  SKIPPED_ATTRIBUTES_ON_COPY = [
6
6
  "cached_tag_list",
7
+ "tag_names",
7
8
  "created_at",
8
9
  "creator_id",
9
10
  "position",
@@ -27,11 +28,13 @@ module Alchemy
27
28
  .merge(differences)
28
29
  .merge(
29
30
  autogenerate_ingredients: false,
30
- autogenerate_nested_elements: false,
31
- tags: source_element.tags
31
+ autogenerate_nested_elements: false
32
32
  )
33
33
 
34
34
  new_element = Element.new(attributes)
35
+ # Only assign tags when the source has any, so copying untagged elements
36
+ # (the common case) does not trigger gutentag's tag reconciliation.
37
+ new_element.tag_list = source_element.tag_list if source_element.tag_list.present?
35
38
  new_element.ingredients = source_element.ingredients.map(&:dup)
36
39
  new_element.save!
37
40
 
@@ -72,8 +72,7 @@ module Alchemy
72
72
  # Associations to preload for element rendering
73
73
  def element_includes
74
74
  [
75
- {ingredients: :related_object},
76
- :tags
75
+ {ingredients: :related_object}
77
76
  ]
78
77
  end
79
78
 
@@ -68,6 +68,7 @@
68
68
  --font-size_xlarge: 24px; /* 2 rem */
69
69
 
70
70
  --font-weight_normal: 500;
71
+ --font-weight_semibold: 600;
71
72
  --font-weight_bold: 700;
72
73
 
73
74
  --font-default: var(--font-size_medium) var(--font-sans);
@@ -53,11 +53,3 @@
53
53
  cursor: not-allowed;
54
54
  }
55
55
  }
56
-
57
- %gradiated-toolbar {
58
- display: flex;
59
- background: var(--toolbar-bg-color);
60
- padding: var(--toolbar-padding);
61
- height: var(--toolbar-height);
62
- border-bottom: var(--toolbar-border-bottom);
63
- }
@@ -152,8 +152,10 @@
152
152
  --pagination-disabled-text-color: var(--a-light-grey);
153
153
 
154
154
  /* Panels */
155
- --panel-background-color: var(--a-darker-grey);
155
+ --panel-background-color: var(--body-background-color);
156
156
  --panel-border-color: var(--a-grey);
157
+ --panel-box-shadow: 0 var(--spacing-0) var(--spacing-2)
158
+ hsla(0deg, 0%, 0%, 0.5);
157
159
 
158
160
  --picture-thumbnail-background-color: var(--a-dark-grey);
159
161
  --picture-thumbnail-toolbar-bg-color: var(--a-darker-grey);
@@ -172,6 +174,8 @@
172
174
  --select-hover-bg-color: var(--color-blue_dark);
173
175
  --select-hover-border-color: var(--form-field-border-color);
174
176
  --select-hover-text-color: var(--text-color);
177
+ --select-selected-bg-color: var(--a-grey);
178
+ --select-selected-marker-color: var(--color-blue_medium);
175
179
  --select-background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="hsl(200deg, 5%, 70%)" viewBox="0 0 24 24"><path d="M11.9997 13.1714L16.9495 8.22168L18.3637 9.63589L11.9997 15.9999L5.63574 9.63589L7.04996 8.22168L11.9997 13.1714Z"></path></svg>');
176
180
  --select-close-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsl(200deg, 5%, 70%)"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path></svg>');
177
181
  --select-disabled-background-color: transparent;
@@ -284,12 +288,6 @@
284
288
  --sl-color-neutral-800: var(--a-lightest-grey);
285
289
  --sl-color-neutral-900: var(--a-lightest-grey);
286
290
  --sl-color-neutral-950: var(--a-lightest-grey);
287
-
288
- .select2-container.select2-container-multi
289
- .select2-search-choice-close:before,
290
- .select2-container.select2-allowclear .select2-search-choice-close:before {
291
- background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsl(200deg, 5%, 70%)"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path></svg>');
292
- }
293
291
  }
294
292
 
295
293
  @mixin light-theme {
@@ -434,8 +432,9 @@
434
432
  --pagination-disabled-text-color: #c0c0c0;
435
433
 
436
434
  /* Panels */
437
- --panel-background-color: var(--color-grey_light);
438
- --panel-border-color: var(--border-color);
435
+ --panel-background-color: var(--body-background-color);
436
+ --panel-border-color: var(--color-grey_light);
437
+ --panel-box-shadow: var(--dashboard-widget-box-shadow);
439
438
 
440
439
  --picture-tool-background-color: var(--color-white);
441
440
  --picture-tool-hover-background-color: var(--color-grey_very_light);
@@ -452,6 +451,8 @@
452
451
  --select-hover-bg-color: var(--color-blue_dark);
453
452
  --select-hover-border-color: var(--border-color);
454
453
  --select-hover-text-color: var(--color-white);
454
+ --select-selected-bg-color: var(--color-grey_light);
455
+ --select-selected-marker-color: var(--color-blue_medium);
455
456
  --select-background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsla(224deg, 23%, 26%, 0.75)"><path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg>');
456
457
  --select-close-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsla(224deg, 23%, 26%, 0.75)"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path></svg>');
457
458
  --select-disabled-background-color: transparent;
@@ -0,0 +1,29 @@
1
+ .empty-state {
2
+ width: 100%;
3
+ display: flex;
4
+ flex-direction: column;
5
+ align-items: center;
6
+ gap: var(--spacing-2);
7
+ }
8
+
9
+ .empty-state-header {
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: center;
13
+ gap: var(--spacing-2);
14
+ margin-top: var(--spacing-2);
15
+ width: 100%;
16
+
17
+ > svg {
18
+ width: var(--icon-size-xl);
19
+ height: auto;
20
+ }
21
+
22
+ .empty-state-notice {
23
+ text-align: center;
24
+ padding-bottom: var(--spacing-4);
25
+ border-bottom: 1px solid var(--panel-border-color);
26
+ margin-bottom: var(--spacing-4);
27
+ text-shadow: var(--text-shadow-light);
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ .help-container {
2
+ display: grid;
3
+ grid-template-columns: 1fr 1fr;
4
+ }
5
+
6
+ .help-resource-links {
7
+ display: grid;
8
+ grid-template-columns: 1fr 1fr;
9
+ gap: var(--spacing-4);
10
+ }