alchemy_cms 8.1.9 → 8.2.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -1
  3. data/app/assets/builds/alchemy/admin.css +1 -1
  4. data/app/assets/builds/alchemy/alchemy_admin.min.js +2 -0
  5. data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -0
  6. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  7. data/app/assets/builds/alchemy/light-theme.css +1 -1
  8. data/app/assets/builds/alchemy/theme.css +1 -1
  9. data/app/assets/builds/alchemy/welcome.css +1 -1
  10. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  11. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  12. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  13. data/app/components/alchemy/admin/current_user_name.rb +34 -0
  14. data/app/components/alchemy/admin/locale_select.rb +12 -8
  15. data/app/components/alchemy/admin/page_node.html.erb +3 -2
  16. data/app/components/alchemy/admin/picture_thumbnail.rb +1 -1
  17. data/app/components/alchemy/admin/preview_time_select.rb +55 -0
  18. data/app/components/alchemy/admin/publish_element_button.html.erb +41 -0
  19. data/app/components/alchemy/admin/publish_element_button.rb +13 -0
  20. data/app/components/alchemy/admin/timezone_select.rb +47 -0
  21. data/app/components/alchemy/ingredients/select_editor.rb +6 -1
  22. data/app/controllers/alchemy/admin/base_controller.rb +1 -0
  23. data/app/controllers/alchemy/admin/elements_controller.rb +54 -34
  24. data/app/controllers/alchemy/admin/pages_controller.rb +1 -0
  25. data/app/controllers/alchemy/admin/resources_controller.rb +11 -6
  26. data/app/controllers/alchemy/pages_controller.rb +1 -2
  27. data/app/helpers/alchemy/admin/base_helper.rb +4 -7
  28. data/app/helpers/alchemy/url_helper.rb +2 -10
  29. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +28 -27
  30. data/app/javascript/alchemy_admin/components/element_editor.js +11 -2
  31. data/app/javascript/alchemy_admin/components/message.js +5 -1
  32. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +5 -5
  33. data/app/javascript/alchemy_admin/image_cropper.js +10 -6
  34. data/app/javascript/alchemy_admin/initializer.js +6 -33
  35. data/app/javascript/alchemy_admin/shoelace_theme.js +6 -2
  36. data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
  37. data/app/javascript/alchemy_admin.js +12 -2
  38. data/app/models/alchemy/attachment.rb +1 -1
  39. data/app/models/alchemy/current.rb +5 -1
  40. data/app/models/alchemy/element/element_ingredients.rb +11 -3
  41. data/app/models/alchemy/element.rb +10 -0
  42. data/app/models/alchemy/ingredient.rb +2 -0
  43. data/app/models/alchemy/ingredients/select.rb +1 -2
  44. data/app/models/alchemy/page/etag_generator.rb +21 -0
  45. data/app/models/alchemy/page/url_path.rb +11 -2
  46. data/app/models/alchemy/page.rb +12 -2
  47. data/app/models/alchemy/page_version.rb +5 -5
  48. data/app/models/alchemy/picture.rb +19 -2
  49. data/app/models/alchemy/storage_adapter/active_storage.rb +9 -0
  50. data/app/models/alchemy/storage_adapter/dragonfly.rb +9 -0
  51. data/app/models/alchemy/storage_adapter.rb +1 -0
  52. data/app/models/concerns/alchemy/publishable.rb +20 -12
  53. data/app/models/concerns/alchemy/relatable_resource.rb +16 -2
  54. data/app/models/concerns/alchemy/touch_elements.rb +3 -3
  55. data/app/services/alchemy/element_preloader.rb +107 -0
  56. data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
  57. data/app/stylesheets/alchemy/_mixins.scss +1 -1
  58. data/app/stylesheets/alchemy/_themes.scss +2 -0
  59. data/app/stylesheets/alchemy/admin/base.scss +2 -1
  60. data/app/stylesheets/alchemy/admin/elements.scss +22 -19
  61. data/app/stylesheets/alchemy/admin/form_fields.scss +3 -0
  62. data/app/stylesheets/alchemy/admin/forms.scss +14 -1
  63. data/app/stylesheets/alchemy/admin/frame.scss +9 -8
  64. data/app/stylesheets/alchemy/admin/notices.scss +1 -1
  65. data/app/stylesheets/alchemy/admin/popover.scss +37 -0
  66. data/app/stylesheets/alchemy/admin/selects.scss +4 -0
  67. data/app/stylesheets/alchemy/admin/shoelace.scss +16 -4
  68. data/app/stylesheets/alchemy/admin/toolbar.scss +8 -0
  69. data/app/stylesheets/alchemy/admin.scss +1 -0
  70. data/app/views/alchemy/admin/_header.html.erb +4 -0
  71. data/app/views/alchemy/admin/_left_menu.html.erb +24 -0
  72. data/app/views/alchemy/admin/_main_navi.html.erb +6 -0
  73. data/app/views/alchemy/admin/_top_menu.html.erb +6 -0
  74. data/app/views/alchemy/admin/_user_info.html.erb +5 -0
  75. data/app/views/alchemy/admin/crop.html.erb +6 -11
  76. data/app/views/alchemy/admin/elements/_header.html.erb +16 -6
  77. data/app/views/alchemy/admin/elements/_schedule.html.erb +62 -0
  78. data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -15
  79. data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +28 -0
  80. data/app/views/alchemy/admin/nodes/index.html.erb +1 -1
  81. data/app/views/alchemy/admin/pages/_locked_pages.html.erb +5 -0
  82. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +4 -4
  83. data/app/views/alchemy/admin/pages/_table.html.erb +1 -1
  84. data/app/views/alchemy/admin/pages/edit.html.erb +6 -2
  85. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +10 -10
  86. data/app/views/alchemy/admin/partials/_site_select.html.erb +6 -3
  87. data/app/views/alchemy/admin/pictures/index.html.erb +2 -2
  88. data/app/views/alchemy/admin/tinymce/_setup.html.erb +9 -16
  89. data/app/views/alchemy/admin/uploader/_setup.html.erb +1 -6
  90. data/app/views/alchemy/language_links/_language.html.erb +1 -2
  91. data/app/views/layouts/alchemy/admin.html.erb +2 -45
  92. data/config/importmap.rb +7 -2
  93. data/config/locales/alchemy.en.yml +35 -5
  94. data/lib/alchemy/admin/preview_time.rb +23 -0
  95. data/lib/alchemy/admin/preview_url.rb +13 -2
  96. data/lib/alchemy/admin/timezone.rb +56 -0
  97. data/lib/alchemy/configuration.rb +2 -0
  98. data/lib/alchemy/configurations/main.rb +13 -1
  99. data/lib/alchemy/tasks/tidy.rb +6 -7
  100. data/lib/alchemy/test_support/factories/element_factory.rb +2 -2
  101. data/lib/alchemy/test_support/relatable_resource_examples.rb +2 -2
  102. data/lib/alchemy/test_support/shared_publishable_examples.rb +44 -2
  103. data/lib/alchemy/upgrader.rb +3 -1
  104. data/lib/alchemy/version.rb +1 -1
  105. data/lib/alchemy_cms.rb +2 -0
  106. data/lib/generators/alchemy/install/install_generator.rb +2 -1
  107. data/vendor/javascript/handlebars.min.js +4 -4
  108. data/vendor/javascript/shoelace.min.js +1419 -1323
  109. data/vendor/javascript/sortable.min.js +2 -2
  110. data/vendor/javascript/tinymce.min.js +1 -1
  111. metadata +35 -1
@@ -20,20 +20,29 @@ module Alchemy
20
20
  # link_to page.url
21
21
  #
22
22
  class UrlPath
23
- def initialize(page)
23
+ def initialize(page, optional_params = {})
24
24
  @page = page
25
25
  @language = @page.language
26
26
  @site = @language.site
27
+ @optional_params = optional_params
27
28
  end
28
29
 
29
30
  def call
30
- if @page.language_root?
31
+ path = if @page.language_root?
31
32
  language_root_path
32
33
  elsif @site.languages.count(&:public?) > 1
33
34
  page_path_with_language_prefix
34
35
  else
35
36
  page_path_with_leading_slash
36
37
  end
38
+
39
+ if @optional_params.present?
40
+ uri = URI(path)
41
+ uri.query = @optional_params.to_query
42
+ uri.to_s
43
+ else
44
+ path
45
+ end
37
46
  end
38
47
 
39
48
  private
@@ -135,6 +135,8 @@ module Alchemy
135
135
 
136
136
  has_many :page_ingredients, class_name: "Alchemy::Ingredients::Page", foreign_key: :related_object_id, dependent: :nullify
137
137
 
138
+ before_destroy :check_descendants_for_menu_nodes
139
+
138
140
  before_validation :set_language,
139
141
  if: -> { language.nil? }
140
142
 
@@ -303,8 +305,8 @@ module Alchemy
303
305
  # = The url_path for this page
304
306
  #
305
307
  # @see Alchemy::Page::UrlPath#call
306
- def url_path
307
- self.class.url_path_class.new(self).call
308
+ def url_path(optional_params = {})
309
+ self.class.url_path_class.new(self, optional_params).call
308
310
  end
309
311
 
310
312
  # The page's view partial is dependent from its page layout
@@ -611,5 +613,13 @@ module Alchemy
611
613
  ids = node_ids + nodes.flat_map { |n| n.ancestors.pluck(:id) }
612
614
  Node.where(id: ids).touch_all
613
615
  end
616
+
617
+ def check_descendants_for_menu_nodes
618
+ pages_with_nodes = descendants.joins(:nodes).reorder("alchemy_pages.lft").distinct
619
+ if pages_with_nodes.exists?
620
+ errors.add(:descendants, :still_attached_to_nodes, page_names: pages_with_nodes.map(&:name).to_sentence)
621
+ throw :abort
622
+ end
623
+ end
614
624
  end
615
625
  end
@@ -37,21 +37,21 @@ module Alchemy
37
37
  #
38
38
  # @param time [DateTime] (Time.current)
39
39
  # @returns Boolean
40
- def public?(time = Time.current)
40
+ def public?(time = Current.preview_time)
41
41
  already_public_for?(time) && still_public_for?(time)
42
42
  end
43
43
 
44
44
  # Determines if this version is already public for given time
45
- # @param time [DateTime] (Time.current)
45
+ # @param time [DateTime] (Current.preview_time)
46
46
  # @returns Boolean
47
- def already_public_for?(time = Time.current)
47
+ def already_public_for?(time = Current.preview_time)
48
48
  !public_on.nil? && public_on <= time
49
49
  end
50
50
 
51
51
  # Determines if this version is still public for given time
52
- # @param time [DateTime] (Time.current)
52
+ # @param time [DateTime] (Current.preview_time)
53
53
  # @returns Boolean
54
- def still_public_for?(time = Time.current)
54
+ def still_public_for?(time = Current.preview_time)
55
55
  public_until.nil? || public_until >= time
56
56
  end
57
57
 
@@ -133,6 +133,16 @@ module Alchemy
133
133
  def file_formats(scope = all)
134
134
  Alchemy.storage_adapter.file_formats(name, scope:)
135
135
  end
136
+
137
+ # Preload associations for element editor display
138
+ #
139
+ # @param pictures [Array<Picture>] Collection of pictures to preload for
140
+ def alchemy_element_preloads(pictures)
141
+ return if pictures.blank?
142
+
143
+ # Preload storage-specific associations to avoid N+1 when rendering thumbnails
144
+ Alchemy.storage_adapter.preload_picture_associations(pictures)
145
+ end
136
146
  end
137
147
 
138
148
  # Instance methods
@@ -183,8 +193,15 @@ module Alchemy
183
193
  end
184
194
 
185
195
  # Returns the picture description for a given language.
196
+ #
197
+ # @param language [Language] The language to get description for
198
+ # @return [String, nil] The description text or nil
186
199
  def description_for(language)
187
- descriptions.find_by(language: language)&.text
200
+ if descriptions.loaded?
201
+ descriptions.detect { _1.language == language }&.text
202
+ else
203
+ descriptions.find_by(language: language)&.text
204
+ end
188
205
  end
189
206
 
190
207
  # Returns an uri escaped name.
@@ -236,7 +253,7 @@ module Alchemy
236
253
  # even if it is also assigned on a restricted page.
237
254
  #
238
255
  def restricted?
239
- pages.any? && pages.not_restricted.blank?
256
+ related_pages.any? && related_pages.not_restricted.blank?
240
257
  end
241
258
 
242
259
  def image_file_name
@@ -188,6 +188,15 @@ module Alchemy
188
188
  pictures.with_attached_image_file
189
189
  end
190
190
 
191
+ # Preload picture associations on already-loaded records
192
+ # @param [Array<Alchemy::Picture>] pictures
193
+ def preload_picture_associations(pictures)
194
+ ActiveRecord::Associations::Preloader.new(
195
+ records: pictures,
196
+ associations: {image_file_attachment: :blob}
197
+ ).call
198
+ end
199
+
191
200
  # @param [Alchemy::Attachment]
192
201
  # @return [TrueClass, FalseClass]
193
202
  def set_attachment_name?(attachment)
@@ -213,6 +213,15 @@ module Alchemy
213
213
  pictures.includes(:thumbs)
214
214
  end
215
215
 
216
+ # Preload picture associations on already-loaded records
217
+ # @param [Array<Alchemy::Picture>] pictures
218
+ def preload_picture_associations(pictures)
219
+ ActiveRecord::Associations::Preloader.new(
220
+ records: pictures,
221
+ associations: :thumbs
222
+ ).call
223
+ end
224
+
216
225
  # @param [Alchemy::Attachment]
217
226
  # @return [TrueClass, FalseClass]
218
227
  def set_attachment_name?(attachment)
@@ -25,6 +25,7 @@ module Alchemy
25
25
  :image_file_size,
26
26
  :image_file_width,
27
27
  :picture_url_class,
28
+ :preload_picture_associations,
28
29
  :preloaded_pictures,
29
30
  :preprocessor_class,
30
31
  :ransackable_associations,
@@ -6,13 +6,21 @@ module Alchemy
6
6
  scope :draft, -> { where(public_on: nil) }
7
7
  scope :scheduled, -> { where.not(public_on: nil) }
8
8
 
9
- scope :published, ->(at: Time.current) {
9
+ scope :published, ->(at: Current.preview_time) {
10
10
  scheduled
11
11
  .where("#{table_name}.public_on <= :at", at:)
12
12
  .where(public_until: nil).or(
13
13
  where("#{table_name}.public_until > :at", at:)
14
14
  )
15
15
  }
16
+
17
+ validate do
18
+ if public_on.present? && public_until.present?
19
+ if public_until <= public_on
20
+ errors.add(:public_until, :must_be_after_public_on)
21
+ end
22
+ end
23
+ end
16
24
  end
17
25
 
18
26
  # Determines if this record is public
@@ -21,15 +29,15 @@ module Alchemy
21
29
  # and returns true if the time given (+Time.current+ per default)
22
30
  # is in this timespan.
23
31
  #
24
- # @param time [DateTime] (Time.current)
32
+ # @param at [DateTime] (Time.current)
25
33
  # @returns Boolean
26
- def public?(time = Time.current)
27
- already_public_for?(time) && still_public_for?(time)
34
+ def public?(at: Current.preview_time)
35
+ already_public_for?(at:) && still_public_for?(at:)
28
36
  end
29
37
  alias_method :public, :public?
30
38
 
31
- def scheduled?
32
- public_on&.future? || public_until&.future?
39
+ def scheduled?(at: Current.preview_time)
40
+ (public_on.present? && public_on > at) || (public_until.present? && public_until > at)
33
41
  end
34
42
 
35
43
  # Determines if this record is publishable
@@ -42,17 +50,17 @@ module Alchemy
42
50
  end
43
51
 
44
52
  # Determines if this record is already public for given time
45
- # @param time [DateTime] (Time.current)
53
+ # @param at [DateTime] (Time.current)
46
54
  # @returns Boolean
47
- def already_public_for?(time = Time.current)
48
- !public_on.nil? && public_on <= time
55
+ def already_public_for?(at: Current.preview_time)
56
+ !public_on.nil? && public_on <= at
49
57
  end
50
58
 
51
59
  # Determines if this record is still public for given time
52
- # @param time [DateTime] (Time.current)
60
+ # @param at [DateTime] (Time.current)
53
61
  # @returns Boolean
54
- def still_public_for?(time = Time.current)
55
- public_until.nil? || public_until >= time
62
+ def still_public_for?(at: Current.preview_time)
63
+ public_until.nil? || public_until > at
56
64
  end
57
65
  end
58
66
  end
@@ -2,6 +2,20 @@ module Alchemy
2
2
  module RelatableResource
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ class_methods do
6
+ # Preload associations for element editor display
7
+ #
8
+ # Override this method in models that need custom preloading
9
+ # when displayed in the element editor (e.g., preloading
10
+ # picture thumbnails or products).
11
+ #
12
+ # @param records [Array] Collection of records to preload for
13
+ def alchemy_element_preloads(records)
14
+ # Default implementation does nothing
15
+ # Override in subclasses that need preloading
16
+ end
17
+ end
18
+
5
19
  included do
6
20
  scope :deletable, -> do
7
21
  where(
@@ -15,8 +29,8 @@ module Alchemy
15
29
  foreign_key: "related_object_id",
16
30
  as: :related_object
17
31
 
18
- has_many :elements, through: :related_ingredients
19
- has_many :pages, through: :elements
32
+ has_many :related_elements, through: :related_ingredients, source: :element
33
+ has_many :related_pages, through: :related_elements, source: :page
20
34
  end
21
35
 
22
36
  # Returns true if object is not assigned to any ingredient.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- # If the model has a +elements+ association,
4
+ # If the model has a +related_elements+ association,
5
5
  # it updates all their timestamps after save.
6
6
  #
7
7
  # Should only be used on bottom to top relations,
@@ -16,9 +16,9 @@ module Alchemy
16
16
  private
17
17
 
18
18
  def touch_elements
19
- return unless respond_to?(:elements)
19
+ return unless respond_to?(:related_elements)
20
20
 
21
- elements.touch_all
21
+ related_elements.touch_all
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # Preloads element trees with all associations and nested elements
5
+ #
6
+ # This service efficiently loads element trees to avoid N+1 queries.
7
+ # It recursively preloads all nested elements to unlimited depth.
8
+ #
9
+ # @example Preload elements for a page version
10
+ # preloader = Alchemy::ElementPreloader.new(page_version: page_version)
11
+ # preloaded_elements = preloader.call
12
+ #
13
+ class ElementPreloader
14
+ # @param page_version [PageVersion] The page version to preload elements for
15
+ def initialize(page_version:)
16
+ @page_version = page_version
17
+ ActiveRecord::Associations::Preloader.new(
18
+ records: [page_version],
19
+ associations: {page: :language}
20
+ ).call
21
+ end
22
+
23
+ # Preloads and returns the element tree with all associations loaded
24
+ #
25
+ # @return [Array<Element>] Elements with preloaded nested elements
26
+ def call
27
+ # Load all elements for the page version with associations
28
+ all_elements = load_all_elements
29
+ return [] if all_elements.empty?
30
+
31
+ # Build parent -> children lookup and populate associations
32
+ populate_nested_associations(all_elements)
33
+
34
+ # Root elements are those without a parent
35
+ root_elements = all_elements.values
36
+ .select { |e| e.parent_element_id.nil? }
37
+ .sort_by(&:position)
38
+ return [] if root_elements.empty?
39
+
40
+ preload_related_objects(root_elements)
41
+
42
+ root_elements
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :page_version
48
+
49
+ # Load all elements for the page version and preload their associations
50
+ def load_all_elements
51
+ Element
52
+ .where(page_version_id: page_version.id)
53
+ .includes(*element_includes)
54
+ .index_by(&:id)
55
+ end
56
+
57
+ # Populate the all_nested_elements association for each element
58
+ def populate_nested_associations(elements_by_id)
59
+ # Group elements by parent_id
60
+ elements_by_parent = elements_by_id.values.group_by(&:parent_element_id)
61
+
62
+ elements_by_id.each_value do |element|
63
+ children = elements_by_parent[element.id] || []
64
+ children = children.sort_by(&:position)
65
+
66
+ # Manually set the association target
67
+ element.association(:all_nested_elements).target = children
68
+ element.association(:all_nested_elements).loaded!
69
+ end
70
+ end
71
+
72
+ # Associations to preload for element rendering
73
+ def element_includes
74
+ [
75
+ {ingredients: :related_object},
76
+ :tags
77
+ ]
78
+ end
79
+
80
+ # Preload related objects for all ingredients in elements
81
+ # Allows related objects to preload their associations (e.g., picture thumbnails)
82
+ def preload_related_objects(root_elements)
83
+ related_objects_by_class = collect_related_objects(root_elements)
84
+ return if related_objects_by_class.empty?
85
+
86
+ related_objects_by_class.each do |klass, objects|
87
+ if klass.respond_to?(:alchemy_element_preloads)
88
+ klass.alchemy_element_preloads(objects)
89
+ end
90
+ end
91
+ end
92
+
93
+ # Collect unique related objects from element tree, grouped by class
94
+ def collect_related_objects(elements, collected = Hash.new { |h, k| h[k] = {} })
95
+ elements.each do |element|
96
+ element.ingredients.each do |ingredient|
97
+ obj = ingredient.related_object
98
+ collected[obj.class][obj.id] = obj if obj
99
+ end
100
+ if element.association(:all_nested_elements).loaded?
101
+ collect_related_objects(element.all_nested_elements, collected)
102
+ end
103
+ end
104
+ collected.transform_values(&:values)
105
+ end
106
+ end
107
+ end
@@ -155,6 +155,7 @@
155
155
  /* Select */
156
156
  --select-medium-width: 90px;
157
157
  --select-large-width: 120px;
158
+ --select-x-large-width: 180px;
158
159
 
159
160
  /* Sidebar */
160
161
  --sidebar-width: 232px;
@@ -6,7 +6,7 @@
6
6
  border-color: $border-color;
7
7
  border-radius: $border-radius;
8
8
  box-shadow: $box-shadow;
9
- outline: none;
9
+ outline: 0 none;
10
10
 
11
11
  &::-moz-focus-inner {
12
12
  border: none !important;
@@ -24,6 +24,7 @@
24
24
  --button-secondary-hover-bg-color: var(--a-dark-grey);
25
25
  --button-secondary-hover-border-color: var(--a-grey);
26
26
  --button-secondary-text-color: var(--color-white);
27
+ --button-primary-text-color: var(--a-darker-grey);
27
28
 
28
29
  --code-background-color: var(--a-darkest-grey);
29
30
  --code-border-color: var(--a-grey);
@@ -309,6 +310,7 @@
309
310
  --button-secondary-hover-bg-color: var(--color-white);
310
311
  --button-secondary-hover-border-color: var(--color-grey_medium);
311
312
  --button-secondary-text-color: var(--text-color);
313
+ --button-primary-text-color: var(--color-white);
312
314
 
313
315
  --code-background-color: var(--color-grey_light);
314
316
  --code-border-color: var(--border-color);
@@ -177,7 +177,8 @@ a img {
177
177
  width: 100%;
178
178
  }
179
179
 
180
- .hidden {
180
+ .hidden,
181
+ [hidden] {
181
182
  display: none !important;
182
183
  }
183
184
 
@@ -237,21 +237,17 @@ button.element-toggle {
237
237
  .element-toggle {
238
238
  margin-left: 0;
239
239
  }
240
-
241
- .element-hidden-icon {
242
- display: inline-flex;
243
- align-items: center;
244
- gap: var(--spacing-1);
245
- margin-left: auto;
246
- }
247
-
248
- .element-hidden-label {
249
- line-height: 1;
250
- font-size: var(--font-size_small);
251
- }
252
240
  }
253
241
  }
254
242
 
243
+ .element-status-icons {
244
+ display: inline-flex;
245
+ align-items: center;
246
+ gap: var(--spacing-1);
247
+ margin-left: auto;
248
+ font-size: var(--font-size_small);
249
+ }
250
+
255
251
  &.is-fixed {
256
252
  border-width: 0;
257
253
  border-radius: 0;
@@ -355,10 +351,6 @@ button.element-toggle {
355
351
  }
356
352
 
357
353
  &.compact {
358
- .element-hidden-label {
359
- display: none;
360
- }
361
-
362
354
  .element-toolbar {
363
355
  visibility: hidden;
364
356
  position: absolute;
@@ -437,6 +429,16 @@ button.element-toggle {
437
429
  transform 1s ease-in;
438
430
  }
439
431
 
432
+ .alchemy-popover {
433
+ width: 360px;
434
+ padding-bottom: var(--spacing-0);
435
+
436
+ label {
437
+ margin-bottom: var(--spacing-1);
438
+ font-size: var(--font-size_small);
439
+ }
440
+ }
441
+
440
442
  .element-body {
441
443
  margin: var(--spacing-2);
442
444
  }
@@ -449,7 +451,7 @@ button.element-toggle {
449
451
  text-align: left;
450
452
  }
451
453
 
452
- alchemy-message {
454
+ > alchemy-message {
453
455
  margin: var(--spacing-2);
454
456
  }
455
457
 
@@ -538,7 +540,7 @@ button.element-toggle {
538
540
  }
539
541
  }
540
542
 
541
- .element-hidden-icon {
543
+ .element-status-icons {
542
544
  display: none;
543
545
  white-space: nowrap;
544
546
  flex-shrink: 0;
@@ -1075,7 +1077,8 @@ select.long {
1075
1077
  .picture_thumbnail,
1076
1078
  .select2-container,
1077
1079
  .tinymce_container {
1078
- outline: 1px solid var(--element-dirty-border-color);
1080
+ box-shadow: 0 0 0 1px var(--element-dirty-border-color);
1081
+ border-color: var(--element-dirty-border-color);
1079
1082
  }
1080
1083
  }
1081
1084
 
@@ -8,6 +8,9 @@ input[type="text"],
8
8
  input[type="email"],
9
9
  input[type="password"],
10
10
  input[type="search"],
11
+ input[type="date"],
12
+ input[type="time"],
13
+ input[type="datetime-local"],
11
14
  .thin_border,
12
15
  .input_field {
13
16
  @extend %default-input-style;
@@ -260,11 +260,24 @@ form {
260
260
  }
261
261
 
262
262
  .submit {
263
+ display: flex;
263
264
  padding: var(--spacing-1) 0;
264
- text-align: right;
265
+ justify-content: space-between;
266
+
267
+ > button:last-of-type {
268
+ margin-left: auto;
269
+ }
270
+ }
271
+
272
+ &.simple_form {
273
+ .buttons {
274
+ margin-top: var(--spacing-1);
275
+ margin-left: var(--form-left-column-width);
276
+ }
265
277
  }
266
278
 
267
279
  .input-column {
280
+ width: 50%;
268
281
  padding: 0 var(--spacing-1);
269
282
 
270
283
  input[type] {
@@ -112,22 +112,23 @@ div#overlay_text_box {
112
112
  border-left: var(--border-default);
113
113
  white-space: nowrap;
114
114
  background-color: var(--toolbar-bg-color);
115
-
116
- select {
117
- background-color: transparent;
118
- border: none;
119
- border-radius: 0;
120
- border-left: var(--border-default);
121
- }
115
+ gap: var(--spacing-1);
122
116
 
123
117
  .current-user-name {
124
118
  display: flex;
125
119
  align-items: center;
126
120
  gap: var(--spacing-1);
127
- padding: 0 var(--spacing-3);
121
+ padding-left: var(--spacing-3);
128
122
 
129
123
  @media screen and (max-width: vars.$large-screen-break-point) {
130
124
  display: none;
131
125
  }
132
126
  }
133
127
  }
128
+
129
+ #admin_locale {
130
+ background-color: transparent;
131
+ border: none;
132
+ border-radius: 0;
133
+ border-left: var(--border-default);
134
+ }
@@ -11,7 +11,7 @@ alchemy-message {
11
11
  word-break: break-word;
12
12
  line-height: 1.5;
13
13
 
14
- turbo-frame & {
14
+ turbo-frame > & {
15
15
  margin: var(--spacing-2);
16
16
  }
17
17
 
@@ -0,0 +1,37 @@
1
+ .alchemy-popover {
2
+ position: relative;
3
+ padding: var(--spacing-2);
4
+
5
+ alchemy-message {
6
+ margin-left: 0;
7
+ margin-right: 0;
8
+ margin-bottom: var(--spacing-3);
9
+ }
10
+
11
+ .timezone-select {
12
+ display: flex;
13
+ gap: var(--spacing-2);
14
+ align-items: center;
15
+
16
+ .alchemy_selectbox {
17
+ min-width: var(--select-x-large-width);
18
+ }
19
+ }
20
+
21
+ span.error {
22
+ display: block;
23
+ background-color: var(--notice-error-background-color);
24
+ color: var(--notice-error-text-color);
25
+ padding: var(--spacing-2);
26
+ border-radius: var(--border-radius_medium);
27
+ font-size: var(--font-size_small);
28
+ }
29
+
30
+ .submit {
31
+ gap: var(--spacing-2);
32
+
33
+ button {
34
+ width: 50%;
35
+ }
36
+ }
37
+ }
@@ -38,6 +38,10 @@ select {
38
38
  &.large {
39
39
  width: var(--select-large-width);
40
40
  }
41
+
42
+ &.x-large {
43
+ width: var(--select-x-large-width);
44
+ }
41
45
  }
42
46
 
43
47
  .select2-container {